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 'FLEXIBLE_CHECKSUMS_REQ_CRC32': 'U', 

72 'FLEXIBLE_CHECKSUMS_REQ_CRC32C': 'V', 

73 'FLEXIBLE_CHECKSUMS_REQ_CRC64': 'W', 

74 'FLEXIBLE_CHECKSUMS_REQ_SHA1': 'X', 

75 'FLEXIBLE_CHECKSUMS_REQ_SHA256': 'Y', 

76 'FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED': 'Z', 

77 'FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED': 'a', 

78 'FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED': 'b', 

79 'FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED': 'c', 

80 'RESOLVED_ACCOUNT_ID': 'T', 

81} 

82 

83 

84def register_feature_id(feature_id): 

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

86 

87 :type feature_id: str 

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

89 in the ``_USERAGENT_FEATURE_MAPPINGS`` dict. 

90 """ 

91 ctx = get_context() 

92 if ctx is None: 

93 # Never register features outside the scope of a 

94 # ``botocore.context.start_as_current_context`` context manager. 

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

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

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

98 return 

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

100 ctx.features.add(val) 

101 

102 

103def sanitize_user_agent_string_component(raw_str, allow_hash): 

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

105 

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

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

108 

109 :type raw_str: str 

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

111 

112 :type allow_hash: bool 

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

114 """ 

115 return ''.join( 

116 c 

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

118 else '-' 

119 for c in raw_str 

120 ) 

121 

122 

123class UserAgentComponentSizeConfig: 

124 """ 

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

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

127 """ 

128 

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

130 self.max_size_in_bytes = max_size_in_bytes 

131 self.delimiter = delimiter 

132 self._validate_input() 

133 

134 def _validate_input(self): 

135 if self.max_size_in_bytes < 1: 

136 raise ValueError( 

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

138 'Value must be a positive integer.' 

139 ) 

140 

141 

142class UserAgentComponent(NamedTuple): 

143 """ 

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

145 

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

147 In the string representation these are combined in the format 

148 ``prefix/name#value``. 

149 

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

151 built user agent string component. 

152 

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

154 """ 

155 

156 prefix: str 

157 name: str 

158 value: Optional[str] = None 

159 size_config: Optional[UserAgentComponentSizeConfig] = None 

160 

161 def to_string(self): 

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

163 clean_prefix = sanitize_user_agent_string_component( 

164 self.prefix, allow_hash=True 

165 ) 

166 clean_name = sanitize_user_agent_string_component( 

167 self.name, allow_hash=False 

168 ) 

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

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

171 else: 

172 clean_value = sanitize_user_agent_string_component( 

173 self.value, allow_hash=True 

174 ) 

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

176 if self.size_config is not None: 

177 clean_string = self._truncate_string( 

178 clean_string, 

179 self.size_config.max_size_in_bytes, 

180 self.size_config.delimiter, 

181 ) 

182 return clean_string 

183 

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

185 """ 

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

187 equal to ``max_size``. 

188 """ 

189 orig = string 

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

191 parts = string.split(delimiter) 

192 parts.pop() 

193 string = delimiter.join(parts) 

194 

195 if string == '': 

196 logger.debug( 

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

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

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

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

201 ) 

202 return string 

203 

204 

205class RawStringUserAgentComponent: 

206 """ 

207 UserAgentComponent interface wrapper around ``str``. 

208 

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

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

211 performed. 

212 """ 

213 

214 def __init__(self, value): 

215 self._value = value 

216 

217 def to_string(self): 

218 return self._value 

219 

220 

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

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

223try: 

224 from botocore.customizations.useragent import modify_components 

225except ImportError: 

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

227 def modify_components(components): 

228 return components 

229 

230 

231class UserAgentString: 

232 """ 

233 Generator for AWS SDK User-Agent header strings. 

234 

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

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

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

238 string format. 

239 

240 Example usage: 

241 

242 ua_session = UserAgentString.from_environment() 

243 ua_session.set_session_config(...) 

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

245 ua_string = ua_request.to_string() 

246 

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

248 time, the methods can be chained: 

249 

250 ua_string = ( 

251 UserAgentString 

252 .from_environment() 

253 .set_session_config(...) 

254 .with_client_config(Config(...)) 

255 .to_string() 

256 ) 

257 

258 """ 

259 

260 def __init__( 

261 self, 

262 platform_name, 

263 platform_version, 

264 platform_machine, 

265 python_version, 

266 python_implementation, 

267 execution_env, 

268 crt_version=None, 

269 ): 

270 """ 

271 :type platform_name: str 

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

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

274 :type platform_version: str 

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

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

277 :type platform_machine: str 

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

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

280 :type python_version: str 

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

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

283 :type python_implementation: str 

284 :param python_implementation: Name of the python implementation. 

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

286 :type execution_env: str 

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

288 Should be sourced from the ``AWS_EXECUTION_ENV` environment 

289 variable. 

290 :type crt_version: str 

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

292 """ 

293 self._platform_name = platform_name 

294 self._platform_version = platform_version 

295 self._platform_machine = platform_machine 

296 self._python_version = python_version 

297 self._python_implementation = python_implementation 

298 self._execution_env = execution_env 

299 self._crt_version = crt_version 

300 

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

302 self._session_user_agent_name = None 

303 self._session_user_agent_version = None 

304 self._session_user_agent_extra = None 

305 

306 self._client_config = None 

307 

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

309 self._client_features = None 

310 

311 @classmethod 

312 def from_environment(cls): 

313 crt_version = None 

314 if HAS_CRT: 

315 crt_version = _get_crt_version() or 'Unknown' 

316 return cls( 

317 platform_name=platform.system(), 

318 platform_version=platform.release(), 

319 platform_machine=platform.machine(), 

320 python_version=platform.python_version(), 

321 python_implementation=platform.python_implementation(), 

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

323 crt_version=crt_version, 

324 ) 

325 

326 def set_session_config( 

327 self, 

328 session_user_agent_name, 

329 session_user_agent_version, 

330 session_user_agent_extra, 

331 ): 

332 """ 

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

334 

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

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

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

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

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

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

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

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

343 """ 

344 self._session_user_agent_name = session_user_agent_name 

345 self._session_user_agent_version = session_user_agent_version 

346 self._session_user_agent_extra = session_user_agent_extra 

347 return self 

348 

349 def set_client_features(self, features): 

350 """ 

351 Persist client-specific features registered before or during client 

352 creation. 

353 

354 :type features: Set[str] 

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

356 """ 

357 self._client_features = features 

358 

359 def with_client_config(self, client_config): 

360 """ 

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

362 

363 :type client_config: botocore.config.Config 

364 :param client_config: The client configuration object. 

365 """ 

366 cp = copy(self) 

367 cp._client_config = client_config 

368 return cp 

369 

370 def to_string(self): 

371 """ 

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

373 """ 

374 config_ua_override = None 

375 if self._client_config: 

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

377 config_ua_override = self._client_config._supplied_user_agent 

378 else: 

379 config_ua_override = self._client_config.user_agent 

380 

381 if config_ua_override is not None: 

382 return self._build_legacy_ua_string(config_ua_override) 

383 

384 components = [ 

385 *self._build_sdk_metadata(), 

386 RawStringUserAgentComponent('ua/2.1'), 

387 *self._build_os_metadata(), 

388 *self._build_architecture_metadata(), 

389 *self._build_language_metadata(), 

390 *self._build_execution_env_metadata(), 

391 *self._build_feature_metadata(), 

392 *self._build_config_metadata(), 

393 *self._build_app_id(), 

394 *self._build_extra(), 

395 ] 

396 

397 components = modify_components(components) 

398 

399 return ' '.join( 

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

401 ) 

402 

403 def _build_sdk_metadata(self): 

404 """ 

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

406 

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

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

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

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

411 """ 

412 sdk_md = [] 

413 if ( 

414 self._session_user_agent_name 

415 and self._session_user_agent_version 

416 and ( 

417 self._session_user_agent_name != _USERAGENT_SDK_NAME 

418 or self._session_user_agent_version != botocore_version 

419 ) 

420 ): 

421 sdk_md.extend( 

422 [ 

423 UserAgentComponent( 

424 self._session_user_agent_name, 

425 self._session_user_agent_version, 

426 ), 

427 UserAgentComponent( 

428 'md', _USERAGENT_SDK_NAME, botocore_version 

429 ), 

430 ] 

431 ) 

432 else: 

433 sdk_md.append( 

434 UserAgentComponent(_USERAGENT_SDK_NAME, botocore_version) 

435 ) 

436 

437 if self._crt_version is not None: 

438 sdk_md.append( 

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

440 ) 

441 

442 return sdk_md 

443 

444 def _build_os_metadata(self): 

445 """ 

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

447 

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

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

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

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

452 

453 String representations of example return values: 

454 * ``os/macos#10.13.6`` 

455 * ``os/linux`` 

456 * ``os/other`` 

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

458 """ 

459 if self._platform_name is None: 

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

461 

462 plt_name_lower = self._platform_name.lower() 

463 if plt_name_lower in _USERAGENT_ALLOWED_OS_NAMES: 

464 os_family = plt_name_lower 

465 elif plt_name_lower in _USERAGENT_PLATFORM_NAME_MAPPINGS: 

466 os_family = _USERAGENT_PLATFORM_NAME_MAPPINGS[plt_name_lower] 

467 else: 

468 os_family = None 

469 

470 if os_family is not None: 

471 return [ 

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

473 ] 

474 else: 

475 return [ 

476 UserAgentComponent('os', 'other'), 

477 UserAgentComponent( 

478 'md', self._platform_name, self._platform_version 

479 ), 

480 ] 

481 

482 def _build_architecture_metadata(self): 

483 """ 

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

485 

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

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

488 """ 

489 if self._platform_machine: 

490 return [ 

491 UserAgentComponent( 

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

493 ) 

494 ] 

495 return [] 

496 

497 def _build_language_metadata(self): 

498 """ 

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

500 

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

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

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

504 

505 String representation of an example return value: 

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

507 """ 

508 lang_md = [ 

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

510 ] 

511 if self._python_implementation: 

512 lang_md.append( 

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

514 ) 

515 return lang_md 

516 

517 def _build_execution_env_metadata(self): 

518 """ 

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

520 

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

522 from the environment variable AWS_EXECUTION_ENV. 

523 """ 

524 if self._execution_env: 

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

526 else: 

527 return [] 

528 

529 def _build_feature_metadata(self): 

530 """ 

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

532 

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

534 comma-separated metric values. 

535 """ 

536 ctx = get_context() 

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

538 client_features = self._client_features or set() 

539 features = client_features.union(context_features) 

540 if not features: 

541 return [] 

542 size_config = UserAgentComponentSizeConfig(1024, ',') 

543 return [ 

544 UserAgentComponent( 

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

546 ) 

547 ] 

548 

549 def _build_config_metadata(self): 

550 """ 

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

552 

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

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

555 added or removed in future versions. 

556 """ 

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

558 return [] 

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

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

561 if self._client_config.endpoint_discovery_enabled: 

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

563 return cfg_md 

564 

565 def _build_app_id(self): 

566 """ 

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

568 

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

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

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

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

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

574 User-Agent header. 

575 """ 

576 if self._client_config and self._client_config.user_agent_appid: 

577 return [ 

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

579 ] 

580 else: 

581 return [] 

582 

583 def _build_extra(self): 

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

585 

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

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

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

589 

590 Preferred ways to inject application-specific information into 

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

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

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

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

595 """ 

596 extra = [] 

597 if self._session_user_agent_extra: 

598 extra.append( 

599 RawStringUserAgentComponent(self._session_user_agent_extra) 

600 ) 

601 if self._client_config and self._client_config.user_agent_extra: 

602 extra.append( 

603 RawStringUserAgentComponent( 

604 self._client_config.user_agent_extra 

605 ) 

606 ) 

607 return extra 

608 

609 def _build_legacy_ua_string(self, config_ua_override): 

610 components = [config_ua_override] 

611 if self._session_user_agent_extra: 

612 components.append(self._session_user_agent_extra) 

613 if self._client_config.user_agent_extra: 

614 components.append(self._client_config.user_agent_extra) 

615 return ' '.join(components) 

616 

617 def rebuild_and_replace_user_agent_handler( 

618 self, operation_name, request, **kwargs 

619 ): 

620 ua_string = self.to_string() 

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

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

623 

624 

625def _get_crt_version(): 

626 """ 

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

628 changes. 

629 """ 

630 try: 

631 import awscrt 

632 

633 return awscrt.__version__ 

634 except AttributeError: 

635 return None