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 "User agent component `%s` could not be truncated to " 

199 "`%s` bytes with delimiter " 

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

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

202 orig, 

203 max_size, 

204 delimiter, 

205 ) 

206 return string 

207 

208 

209class RawStringUserAgentComponent: 

210 """ 

211 UserAgentComponent interface wrapper around ``str``. 

212 

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

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

215 performed. 

216 """ 

217 

218 def __init__(self, value): 

219 self._value = value 

220 

221 def to_string(self): 

222 return self._value 

223 

224 

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

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

227try: 

228 from botocore.customizations.useragent import modify_components 

229except ImportError: 

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

231 def modify_components(components): 

232 return components 

233 

234 

235class UserAgentString: 

236 """ 

237 Generator for AWS SDK User-Agent header strings. 

238 

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

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

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

242 string format. 

243 

244 Example usage: 

245 

246 ua_session = UserAgentString.from_environment() 

247 ua_session.set_session_config(...) 

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

249 ua_string = ua_request.to_string() 

250 

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

252 time, the methods can be chained: 

253 

254 ua_string = ( 

255 UserAgentString 

256 .from_environment() 

257 .set_session_config(...) 

258 .with_client_config(Config(...)) 

259 .to_string() 

260 ) 

261 

262 """ 

263 

264 def __init__( 

265 self, 

266 platform_name, 

267 platform_version, 

268 platform_machine, 

269 python_version, 

270 python_implementation, 

271 execution_env, 

272 crt_version=None, 

273 ): 

274 """ 

275 :type platform_name: str 

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

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

278 :type platform_version: str 

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

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

281 :type platform_machine: str 

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

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

284 :type python_version: str 

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

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

287 :type python_implementation: str 

288 :param python_implementation: Name of the python implementation. 

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

290 :type execution_env: str 

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

292 Should be sourced from the ``AWS_EXECUTION_ENV` environment 

293 variable. 

294 :type crt_version: str 

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

296 """ 

297 self._platform_name = platform_name 

298 self._platform_version = platform_version 

299 self._platform_machine = platform_machine 

300 self._python_version = python_version 

301 self._python_implementation = python_implementation 

302 self._execution_env = execution_env 

303 self._crt_version = crt_version 

304 

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

306 self._session_user_agent_name = None 

307 self._session_user_agent_version = None 

308 self._session_user_agent_extra = None 

309 

310 self._client_config = None 

311 

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

313 self._client_features = None 

314 

315 @classmethod 

316 def from_environment(cls): 

317 crt_version = None 

318 if HAS_CRT: 

319 crt_version = _get_crt_version() or 'Unknown' 

320 return cls( 

321 platform_name=platform.system(), 

322 platform_version=platform.release(), 

323 platform_machine=platform.machine(), 

324 python_version=platform.python_version(), 

325 python_implementation=platform.python_implementation(), 

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

327 crt_version=crt_version, 

328 ) 

329 

330 def set_session_config( 

331 self, 

332 session_user_agent_name, 

333 session_user_agent_version, 

334 session_user_agent_extra, 

335 ): 

336 """ 

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

338 

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

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

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

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

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

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

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

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

347 """ 

348 self._session_user_agent_name = session_user_agent_name 

349 self._session_user_agent_version = session_user_agent_version 

350 self._session_user_agent_extra = session_user_agent_extra 

351 return self 

352 

353 def set_client_features(self, features): 

354 """ 

355 Persist client-specific features registered before or during client 

356 creation. 

357 

358 :type features: Set[str] 

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

360 """ 

361 self._client_features = features 

362 

363 def with_client_config(self, client_config): 

364 """ 

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

366 

367 :type client_config: botocore.config.Config 

368 :param client_config: The client configuration object. 

369 """ 

370 cp = copy(self) 

371 cp._client_config = client_config 

372 return cp 

373 

374 def to_string(self): 

375 """ 

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

377 """ 

378 config_ua_override = None 

379 if self._client_config: 

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

381 config_ua_override = self._client_config._supplied_user_agent 

382 else: 

383 config_ua_override = self._client_config.user_agent 

384 

385 if config_ua_override is not None: 

386 return self._build_legacy_ua_string(config_ua_override) 

387 

388 components = [ 

389 *self._build_sdk_metadata(), 

390 RawStringUserAgentComponent('ua/2.1'), 

391 *self._build_os_metadata(), 

392 *self._build_architecture_metadata(), 

393 *self._build_language_metadata(), 

394 *self._build_execution_env_metadata(), 

395 *self._build_feature_metadata(), 

396 *self._build_config_metadata(), 

397 *self._build_app_id(), 

398 *self._build_extra(), 

399 ] 

400 

401 components = modify_components(components) 

402 

403 return ' '.join( 

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

405 ) 

406 

407 def _build_sdk_metadata(self): 

408 """ 

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

410 

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

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

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

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

415 """ 

416 sdk_md = [] 

417 if ( 

418 self._session_user_agent_name 

419 and self._session_user_agent_version 

420 and ( 

421 self._session_user_agent_name != _USERAGENT_SDK_NAME 

422 or self._session_user_agent_version != botocore_version 

423 ) 

424 ): 

425 sdk_md.extend( 

426 [ 

427 UserAgentComponent( 

428 self._session_user_agent_name, 

429 self._session_user_agent_version, 

430 ), 

431 UserAgentComponent( 

432 'md', _USERAGENT_SDK_NAME, botocore_version 

433 ), 

434 ] 

435 ) 

436 else: 

437 sdk_md.append( 

438 UserAgentComponent(_USERAGENT_SDK_NAME, botocore_version) 

439 ) 

440 

441 if self._crt_version is not None: 

442 sdk_md.append( 

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

444 ) 

445 

446 return sdk_md 

447 

448 def _build_os_metadata(self): 

449 """ 

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

451 

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

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

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

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

456 

457 String representations of example return values: 

458 * ``os/macos#10.13.6`` 

459 * ``os/linux`` 

460 * ``os/other`` 

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

462 """ 

463 if self._platform_name is None: 

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

465 

466 plt_name_lower = self._platform_name.lower() 

467 if plt_name_lower in _USERAGENT_ALLOWED_OS_NAMES: 

468 os_family = plt_name_lower 

469 elif plt_name_lower in _USERAGENT_PLATFORM_NAME_MAPPINGS: 

470 os_family = _USERAGENT_PLATFORM_NAME_MAPPINGS[plt_name_lower] 

471 else: 

472 os_family = None 

473 

474 if os_family is not None: 

475 return [ 

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

477 ] 

478 else: 

479 return [ 

480 UserAgentComponent('os', 'other'), 

481 UserAgentComponent( 

482 'md', self._platform_name, self._platform_version 

483 ), 

484 ] 

485 

486 def _build_architecture_metadata(self): 

487 """ 

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

489 

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

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

492 """ 

493 if self._platform_machine: 

494 return [ 

495 UserAgentComponent( 

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

497 ) 

498 ] 

499 return [] 

500 

501 def _build_language_metadata(self): 

502 """ 

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

504 

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

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

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

508 

509 String representation of an example return value: 

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

511 """ 

512 lang_md = [ 

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

514 ] 

515 if self._python_implementation: 

516 lang_md.append( 

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

518 ) 

519 return lang_md 

520 

521 def _build_execution_env_metadata(self): 

522 """ 

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

524 

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

526 from the environment variable AWS_EXECUTION_ENV. 

527 """ 

528 if self._execution_env: 

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

530 else: 

531 return [] 

532 

533 def _build_feature_metadata(self): 

534 """ 

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

536 

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

538 comma-separated metric values. 

539 """ 

540 ctx = get_context() 

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

542 client_features = self._client_features or set() 

543 features = client_features.union(context_features) 

544 if not features: 

545 return [] 

546 size_config = UserAgentComponentSizeConfig(1024, ',') 

547 return [ 

548 UserAgentComponent( 

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

550 ) 

551 ] 

552 

553 def _build_config_metadata(self): 

554 """ 

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

556 

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

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

559 added or removed in future versions. 

560 """ 

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

562 return [] 

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

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

565 if self._client_config.endpoint_discovery_enabled: 

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

567 return cfg_md 

568 

569 def _build_app_id(self): 

570 """ 

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

572 

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

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

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

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

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

578 User-Agent header. 

579 """ 

580 if self._client_config and self._client_config.user_agent_appid: 

581 return [ 

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

583 ] 

584 else: 

585 return [] 

586 

587 def _build_extra(self): 

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

589 

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

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

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

593 

594 Preferred ways to inject application-specific information into 

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

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

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

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

599 """ 

600 extra = [] 

601 if self._session_user_agent_extra: 

602 extra.append( 

603 RawStringUserAgentComponent(self._session_user_agent_extra) 

604 ) 

605 if self._client_config and self._client_config.user_agent_extra: 

606 extra.append( 

607 RawStringUserAgentComponent( 

608 self._client_config.user_agent_extra 

609 ) 

610 ) 

611 return extra 

612 

613 def _build_legacy_ua_string(self, config_ua_override): 

614 components = [config_ua_override] 

615 if self._session_user_agent_extra: 

616 components.append(self._session_user_agent_extra) 

617 if self._client_config.user_agent_extra: 

618 components.append(self._client_config.user_agent_extra) 

619 return ' '.join(components) 

620 

621 def rebuild_and_replace_user_agent_handler( 

622 self, operation_name, request, **kwargs 

623 ): 

624 ua_string = self.to_string() 

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

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

627 

628 

629def _get_crt_version(): 

630 """ 

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

632 changes. 

633 """ 

634 try: 

635 import awscrt 

636 

637 return awscrt.__version__ 

638 except AttributeError: 

639 return None