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

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

1733 statements  

1# Copyright 2012-2014 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. 

13import base64 

14import binascii 

15import datetime 

16import email.message 

17import functools 

18import hashlib 

19import io 

20import logging 

21import os 

22import random 

23import re 

24import socket 

25import tempfile 

26import time 

27import warnings 

28import weakref 

29from datetime import datetime as _DatetimeClass 

30from ipaddress import ip_address 

31from pathlib import Path 

32from urllib.request import getproxies, proxy_bypass 

33 

34import dateutil.parser 

35from dateutil.tz import tzutc 

36from urllib3.exceptions import LocationParseError 

37 

38import botocore 

39import botocore.awsrequest 

40import botocore.httpsession 

41 

42# IP Regexes retained for backwards compatibility 

43from botocore.compat import ( 

44 HAS_CRT, 

45 HEX_PAT, # noqa: F401 

46 IPV4_PAT, # noqa: F401 

47 IPV4_RE, 

48 IPV6_ADDRZ_PAT, # noqa: F401 

49 IPV6_ADDRZ_RE, 

50 IPV6_PAT, # noqa: F401 

51 LS32_PAT, # noqa: F401 

52 MD5_AVAILABLE, 

53 UNRESERVED_PAT, # noqa: F401 

54 UNSAFE_URL_CHARS, 

55 ZONE_ID_PAT, # noqa: F401 

56 OrderedDict, 

57 get_current_datetime, 

58 get_md5, 

59 get_tzinfo_options, 

60 json, 

61 quote, 

62 urlparse, 

63 urlsplit, 

64 urlunsplit, 

65 zip_longest, 

66) 

67from botocore.exceptions import ( 

68 ClientError, 

69 ConfigNotFound, 

70 ConnectionClosedError, 

71 ConnectTimeoutError, 

72 EndpointConnectionError, 

73 HTTPClientError, 

74 InvalidDNSNameError, 

75 InvalidEndpointConfigurationError, 

76 InvalidExpressionError, 

77 InvalidHostLabelError, 

78 InvalidIMDSEndpointError, 

79 InvalidIMDSEndpointModeError, 

80 InvalidRegionError, 

81 MetadataRetrievalError, 

82 MissingDependencyException, 

83 ReadTimeoutError, 

84 SSOTokenLoadError, 

85 UnsupportedOutpostResourceError, 

86 UnsupportedS3AccesspointConfigurationError, 

87 UnsupportedS3ArnError, 

88 UnsupportedS3ConfigurationError, 

89 UnsupportedS3ControlArnError, 

90 UnsupportedS3ControlConfigurationError, 

91) 

92from botocore.plugin import ( 

93 PluginContext, 

94 reset_plugin_context, 

95 set_plugin_context, 

96) 

97from botocore.useragent import register_feature_id 

98 

99logger = logging.getLogger(__name__) 

100DEFAULT_METADATA_SERVICE_TIMEOUT = 1 

101METADATA_BASE_URL = 'http://169.254.169.254/' 

102METADATA_BASE_URL_IPv6 = 'http://[fd00:ec2::254]/' 

103METADATA_ENDPOINT_MODES = ('ipv4', 'ipv6') 

104 

105# These are chars that do not need to be urlencoded. 

106# Based on rfc2986, section 2.3 

107SAFE_CHARS = '-._~' 

108LABEL_RE = re.compile(r'[a-z0-9][a-z0-9\-]*[a-z0-9]') 

109RETRYABLE_HTTP_ERRORS = ( 

110 ReadTimeoutError, 

111 EndpointConnectionError, 

112 ConnectionClosedError, 

113 ConnectTimeoutError, 

114) 

115S3_ACCELERATE_WHITELIST = ['dualstack'] 

116# In switching events from using service name / endpoint prefix to service 

117# id, we have to preserve compatibility. This maps the instances where either 

118# is different than the transformed service id. 

119EVENT_ALIASES = { 

120 "api.mediatailor": "mediatailor", 

121 "api.pricing": "pricing", 

122 "api.sagemaker": "sagemaker", 

123 "apigateway": "api-gateway", 

124 "application-autoscaling": "application-auto-scaling", 

125 "appstream2": "appstream", 

126 "autoscaling": "auto-scaling", 

127 "autoscaling-plans": "auto-scaling-plans", 

128 "ce": "cost-explorer", 

129 "cloudhsmv2": "cloudhsm-v2", 

130 "cloudsearchdomain": "cloudsearch-domain", 

131 "cognito-idp": "cognito-identity-provider", 

132 "config": "config-service", 

133 "cur": "cost-and-usage-report-service", 

134 "data.iot": "iot-data-plane", 

135 "data.jobs.iot": "iot-jobs-data-plane", 

136 "data.mediastore": "mediastore-data", 

137 "datapipeline": "data-pipeline", 

138 "devicefarm": "device-farm", 

139 "directconnect": "direct-connect", 

140 "discovery": "application-discovery-service", 

141 "dms": "database-migration-service", 

142 "ds": "directory-service", 

143 "dynamodbstreams": "dynamodb-streams", 

144 "elasticbeanstalk": "elastic-beanstalk", 

145 "elasticfilesystem": "efs", 

146 "elasticloadbalancing": "elastic-load-balancing", 

147 "elasticmapreduce": "emr", 

148 "elb": "elastic-load-balancing", 

149 "elbv2": "elastic-load-balancing-v2", 

150 "email": "ses", 

151 "entitlement.marketplace": "marketplace-entitlement-service", 

152 "es": "elasticsearch-service", 

153 "events": "eventbridge", 

154 "cloudwatch-events": "eventbridge", 

155 "iot-data": "iot-data-plane", 

156 "iot-jobs-data": "iot-jobs-data-plane", 

157 "kinesisanalytics": "kinesis-analytics", 

158 "kinesisvideo": "kinesis-video", 

159 "lex-models": "lex-model-building-service", 

160 "lex-runtime": "lex-runtime-service", 

161 "logs": "cloudwatch-logs", 

162 "machinelearning": "machine-learning", 

163 "marketplace-entitlement": "marketplace-entitlement-service", 

164 "marketplacecommerceanalytics": "marketplace-commerce-analytics", 

165 "metering.marketplace": "marketplace-metering", 

166 "meteringmarketplace": "marketplace-metering", 

167 "mgh": "migration-hub", 

168 "models.lex": "lex-model-building-service", 

169 "monitoring": "cloudwatch", 

170 "mturk-requester": "mturk", 

171 "resourcegroupstaggingapi": "resource-groups-tagging-api", 

172 "route53": "route-53", 

173 "route53domains": "route-53-domains", 

174 "runtime.lex": "lex-runtime-service", 

175 "runtime.sagemaker": "sagemaker-runtime", 

176 "sdb": "simpledb", 

177 "secretsmanager": "secrets-manager", 

178 "serverlessrepo": "serverlessapplicationrepository", 

179 "servicecatalog": "service-catalog", 

180 "states": "sfn", 

181 "stepfunctions": "sfn", 

182 "storagegateway": "storage-gateway", 

183 "streams.dynamodb": "dynamodb-streams", 

184 "tagging": "resource-groups-tagging-api", 

185} 

186 

187 

188# This pattern can be used to detect if a header is a flexible checksum header 

189CHECKSUM_HEADER_PATTERN = re.compile( 

190 r'^X-Amz-Checksum-([a-z0-9]*)$', 

191 flags=re.IGNORECASE, 

192) 

193 

194PRIORITY_ORDERED_SUPPORTED_PROTOCOLS = ( 

195 'json', 

196 'rest-json', 

197 'rest-xml', 

198 'smithy-rpc-v2-cbor', 

199 'query', 

200 'ec2', 

201) 

202 

203 

204def ensure_boolean(val): 

205 """Ensures a boolean value if a string or boolean is provided 

206 

207 For strings, the value for True/False is case insensitive 

208 """ 

209 if isinstance(val, bool): 

210 return val 

211 elif isinstance(val, str): 

212 return val.lower() == 'true' 

213 else: 

214 return False 

215 

216 

217def resolve_imds_endpoint_mode(session): 

218 """Resolving IMDS endpoint mode to either IPv6 or IPv4. 

219 

220 ec2_metadata_service_endpoint_mode takes precedence over imds_use_ipv6. 

221 """ 

222 endpoint_mode = session.get_config_variable( 

223 'ec2_metadata_service_endpoint_mode' 

224 ) 

225 if endpoint_mode is not None: 

226 lendpoint_mode = endpoint_mode.lower() 

227 if lendpoint_mode not in METADATA_ENDPOINT_MODES: 

228 error_msg_kwargs = { 

229 'mode': endpoint_mode, 

230 'valid_modes': METADATA_ENDPOINT_MODES, 

231 } 

232 raise InvalidIMDSEndpointModeError(**error_msg_kwargs) 

233 return lendpoint_mode 

234 elif session.get_config_variable('imds_use_ipv6'): 

235 return 'ipv6' 

236 return 'ipv4' 

237 

238 

239def is_json_value_header(shape): 

240 """Determines if the provided shape is the special header type jsonvalue. 

241 

242 :type shape: botocore.shape 

243 :param shape: Shape to be inspected for the jsonvalue trait. 

244 

245 :return: True if this type is a jsonvalue, False otherwise 

246 :rtype: Bool 

247 """ 

248 return ( 

249 hasattr(shape, 'serialization') 

250 and shape.serialization.get('jsonvalue', False) 

251 and shape.serialization.get('location') == 'header' 

252 and shape.type_name == 'string' 

253 ) 

254 

255 

256def has_header(header_name, headers): 

257 """Case-insensitive check for header key.""" 

258 if header_name is None: 

259 return False 

260 elif isinstance(headers, botocore.awsrequest.HeadersDict): 

261 return header_name in headers 

262 else: 

263 return header_name.lower() in [key.lower() for key in headers.keys()] 

264 

265 

266def get_service_module_name(service_model): 

267 """Returns the module name for a service 

268 

269 This is the value used in both the documentation and client class name 

270 """ 

271 name = service_model.metadata.get( 

272 'serviceAbbreviation', 

273 service_model.metadata.get( 

274 'serviceFullName', service_model.service_name 

275 ), 

276 ) 

277 name = name.replace('Amazon', '') 

278 name = name.replace('AWS', '') 

279 name = re.sub(r'\W+', '', name) 

280 return name 

281 

282 

283def normalize_url_path(path): 

284 if not path: 

285 return '/' 

286 return remove_dot_segments(path) 

287 

288 

289def normalize_boolean(val): 

290 """Returns None if val is None, otherwise ensure value 

291 converted to boolean""" 

292 if val is None: 

293 return val 

294 else: 

295 return ensure_boolean(val) 

296 

297 

298def remove_dot_segments(url): 

299 # RFC 3986, section 5.2.4 "Remove Dot Segments" 

300 # Also, AWS services require consecutive slashes to be removed, 

301 # so that's done here as well 

302 if not url: 

303 return '' 

304 input_url = url.split('/') 

305 output_list = [] 

306 for x in input_url: 

307 if x and x != '.': 

308 if x == '..': 

309 if output_list: 

310 output_list.pop() 

311 else: 

312 output_list.append(x) 

313 

314 if url[0] == '/': 

315 first = '/' 

316 else: 

317 first = '' 

318 if url[-1] == '/' and output_list: 

319 last = '/' 

320 else: 

321 last = '' 

322 return first + '/'.join(output_list) + last 

323 

324 

325def validate_jmespath_for_set(expression): 

326 # Validates a limited jmespath expression to determine if we can set a 

327 # value based on it. Only works with dotted paths. 

328 if not expression or expression == '.': 

329 raise InvalidExpressionError(expression=expression) 

330 

331 for invalid in ['[', ']', '*']: 

332 if invalid in expression: 

333 raise InvalidExpressionError(expression=expression) 

334 

335 

336def set_value_from_jmespath(source, expression, value, is_first=True): 

337 # This takes a (limited) jmespath-like expression & can set a value based 

338 # on it. 

339 # Limitations: 

340 # * Only handles dotted lookups 

341 # * No offsets/wildcards/slices/etc. 

342 if is_first: 

343 validate_jmespath_for_set(expression) 

344 

345 bits = expression.split('.', 1) 

346 current_key, remainder = bits[0], bits[1] if len(bits) > 1 else '' 

347 

348 if not current_key: 

349 raise InvalidExpressionError(expression=expression) 

350 

351 if remainder: 

352 if current_key not in source: 

353 # We've got something in the expression that's not present in the 

354 # source (new key). If there's any more bits, we'll set the key 

355 # with an empty dictionary. 

356 source[current_key] = {} 

357 

358 return set_value_from_jmespath( 

359 source[current_key], remainder, value, is_first=False 

360 ) 

361 

362 # If we're down to a single key, set it. 

363 source[current_key] = value 

364 

365 

366def is_global_accesspoint(context): 

367 """Determine if request is intended for an MRAP accesspoint.""" 

368 s3_accesspoint = context.get('s3_accesspoint', {}) 

369 is_global = s3_accesspoint.get('region') == '' 

370 return is_global 

371 

372 

373def create_nested_client(session, service_name, **kwargs): 

374 # If a client is created from within a plugin based on the environment variable, 

375 # an infinite loop could arise. Any clients created from within another client 

376 # must use this method to prevent infinite loops. 

377 ctx = PluginContext(plugins="DISABLED") 

378 token = set_plugin_context(ctx) 

379 try: 

380 return session.create_client(service_name, **kwargs) 

381 finally: 

382 reset_plugin_context(token) 

383 

384 

385class _RetriesExceededError(Exception): 

386 """Internal exception used when the number of retries are exceeded.""" 

387 

388 pass 

389 

390 

391class BadIMDSRequestError(Exception): 

392 def __init__(self, request): 

393 self.request = request 

394 

395 

396class IMDSFetcher: 

397 _RETRIES_EXCEEDED_ERROR_CLS = _RetriesExceededError 

398 _TOKEN_PATH = 'latest/api/token' 

399 _TOKEN_TTL = '21600' 

400 

401 def __init__( 

402 self, 

403 timeout=DEFAULT_METADATA_SERVICE_TIMEOUT, 

404 num_attempts=1, 

405 base_url=METADATA_BASE_URL, 

406 env=None, 

407 user_agent=None, 

408 config=None, 

409 ): 

410 self._timeout = timeout 

411 self._num_attempts = num_attempts 

412 if config is None: 

413 config = {} 

414 self._base_url = self._select_base_url(base_url, config) 

415 self._config = config 

416 

417 if env is None: 

418 env = os.environ.copy() 

419 self._disabled = ( 

420 env.get('AWS_EC2_METADATA_DISABLED', 'false').lower() == 'true' 

421 ) 

422 self._imds_v1_disabled = config.get('ec2_metadata_v1_disabled') 

423 self._user_agent = user_agent 

424 self._session = botocore.httpsession.URLLib3Session( 

425 timeout=self._timeout, 

426 proxies=get_environ_proxies(self._base_url), 

427 ) 

428 

429 def get_base_url(self): 

430 return self._base_url 

431 

432 def _select_base_url(self, base_url, config): 

433 if config is None: 

434 config = {} 

435 

436 requires_ipv6 = ( 

437 config.get('ec2_metadata_service_endpoint_mode') == 'ipv6' 

438 ) 

439 custom_metadata_endpoint = config.get('ec2_metadata_service_endpoint') 

440 

441 if requires_ipv6 and custom_metadata_endpoint: 

442 logger.warning( 

443 "Custom endpoint and IMDS_USE_IPV6 are both set. Using custom endpoint." 

444 ) 

445 

446 chosen_base_url = None 

447 

448 if base_url != METADATA_BASE_URL: 

449 chosen_base_url = base_url 

450 elif custom_metadata_endpoint: 

451 chosen_base_url = custom_metadata_endpoint 

452 elif requires_ipv6: 

453 chosen_base_url = METADATA_BASE_URL_IPv6 

454 else: 

455 chosen_base_url = METADATA_BASE_URL 

456 

457 logger.debug("IMDS ENDPOINT: %s", chosen_base_url) 

458 if not is_valid_uri(chosen_base_url): 

459 raise InvalidIMDSEndpointError(endpoint=chosen_base_url) 

460 

461 return chosen_base_url 

462 

463 def _construct_url(self, path): 

464 sep = '' 

465 if self._base_url and not self._base_url.endswith('/'): 

466 sep = '/' 

467 return f'{self._base_url}{sep}{path}' 

468 

469 def _fetch_metadata_token(self): 

470 self._assert_enabled() 

471 url = self._construct_url(self._TOKEN_PATH) 

472 headers = { 

473 'x-aws-ec2-metadata-token-ttl-seconds': self._TOKEN_TTL, 

474 } 

475 self._add_user_agent(headers) 

476 request = botocore.awsrequest.AWSRequest( 

477 method='PUT', url=url, headers=headers 

478 ) 

479 for i in range(self._num_attempts): 

480 try: 

481 response = self._session.send(request.prepare()) 

482 if response.status_code == 200: 

483 return response.text 

484 elif response.status_code in (404, 403, 405): 

485 return None 

486 elif response.status_code in (400,): 

487 raise BadIMDSRequestError(request) 

488 except ReadTimeoutError: 

489 return None 

490 except RETRYABLE_HTTP_ERRORS as e: 

491 logger.debug( 

492 "Caught retryable HTTP exception while making metadata " 

493 "service request to %s: %s", 

494 url, 

495 e, 

496 exc_info=True, 

497 ) 

498 except HTTPClientError as e: 

499 if isinstance(e.kwargs.get('error'), LocationParseError): 

500 raise InvalidIMDSEndpointError(endpoint=url, error=e) 

501 else: 

502 raise 

503 return None 

504 

505 def _get_request(self, url_path, retry_func, token=None): 

506 """Make a get request to the Instance Metadata Service. 

507 

508 :type url_path: str 

509 :param url_path: The path component of the URL to make a get request. 

510 This arg is appended to the base_url that was provided in the 

511 initializer. 

512 

513 :type retry_func: callable 

514 :param retry_func: A function that takes the response as an argument 

515 and determines if it needs to retry. By default empty and non 

516 200 OK responses are retried. 

517 

518 :type token: str 

519 :param token: Metadata token to send along with GET requests to IMDS. 

520 """ 

521 self._assert_enabled() 

522 if not token: 

523 self._assert_v1_enabled() 

524 if retry_func is None: 

525 retry_func = self._default_retry 

526 url = self._construct_url(url_path) 

527 headers = {} 

528 if token is not None: 

529 headers['x-aws-ec2-metadata-token'] = token 

530 self._add_user_agent(headers) 

531 for i in range(self._num_attempts): 

532 try: 

533 request = botocore.awsrequest.AWSRequest( 

534 method='GET', url=url, headers=headers 

535 ) 

536 response = self._session.send(request.prepare()) 

537 if not retry_func(response): 

538 return response 

539 except RETRYABLE_HTTP_ERRORS as e: 

540 logger.debug( 

541 "Caught retryable HTTP exception while making metadata " 

542 "service request to %s: %s", 

543 url, 

544 e, 

545 exc_info=True, 

546 ) 

547 raise self._RETRIES_EXCEEDED_ERROR_CLS() 

548 

549 def _add_user_agent(self, headers): 

550 if self._user_agent is not None: 

551 headers['User-Agent'] = self._user_agent 

552 

553 def _assert_enabled(self): 

554 if self._disabled: 

555 logger.debug("Access to EC2 metadata has been disabled.") 

556 raise self._RETRIES_EXCEEDED_ERROR_CLS() 

557 

558 def _assert_v1_enabled(self): 

559 if self._imds_v1_disabled: 

560 raise MetadataRetrievalError( 

561 error_msg="Unable to retrieve token for use in IMDSv2 call and IMDSv1 has been disabled" 

562 ) 

563 

564 def _default_retry(self, response): 

565 return self._is_non_ok_response(response) or self._is_empty(response) 

566 

567 def _is_non_ok_response(self, response): 

568 if response.status_code != 200: 

569 self._log_imds_response(response, 'non-200', log_body=True) 

570 return True 

571 return False 

572 

573 def _is_empty(self, response): 

574 if not response.content: 

575 self._log_imds_response(response, 'no body', log_body=True) 

576 return True 

577 return False 

578 

579 def _log_imds_response(self, response, reason_to_log, log_body=False): 

580 statement = ( 

581 "Metadata service returned %s response " 

582 "with status code of %s for url: %s" 

583 ) 

584 logger_args = [reason_to_log, response.status_code, response.url] 

585 if log_body: 

586 statement += ", content body: %s" 

587 logger_args.append(response.content) 

588 logger.debug(statement, *logger_args) 

589 

590 

591class InstanceMetadataFetcher(IMDSFetcher): 

592 _URL_PATH = 'latest/meta-data/iam/security-credentials/' 

593 _REQUIRED_CREDENTIAL_FIELDS = [ 

594 'AccessKeyId', 

595 'SecretAccessKey', 

596 'Token', 

597 'Expiration', 

598 ] 

599 

600 def retrieve_iam_role_credentials(self): 

601 try: 

602 token = self._fetch_metadata_token() 

603 role_name = self._get_iam_role(token) 

604 credentials = self._get_credentials(role_name, token) 

605 if self._contains_all_credential_fields(credentials): 

606 credentials = { 

607 'role_name': role_name, 

608 'access_key': credentials['AccessKeyId'], 

609 'secret_key': credentials['SecretAccessKey'], 

610 'token': credentials['Token'], 

611 'expiry_time': credentials['Expiration'], 

612 } 

613 self._evaluate_expiration(credentials) 

614 return credentials 

615 else: 

616 # IMDS can return a 200 response that has a JSON formatted 

617 # error message (i.e. if ec2 is not trusted entity for the 

618 # attached role). We do not necessarily want to retry for 

619 # these and we also do not necessarily want to raise a key 

620 # error. So at least log the problematic response and return 

621 # an empty dictionary to signal that it was not able to 

622 # retrieve credentials. These error will contain both a 

623 # Code and Message key. 

624 if 'Code' in credentials and 'Message' in credentials: 

625 logger.debug( 

626 'Error response received when retrieving' 

627 'credentials: %s.', 

628 credentials, 

629 ) 

630 return {} 

631 except self._RETRIES_EXCEEDED_ERROR_CLS: 

632 logger.debug( 

633 "Max number of attempts exceeded (%s) when " 

634 "attempting to retrieve data from metadata service.", 

635 self._num_attempts, 

636 ) 

637 except BadIMDSRequestError as e: 

638 logger.debug("Bad IMDS request: %s", e.request) 

639 return {} 

640 

641 def _get_iam_role(self, token=None): 

642 return self._get_request( 

643 url_path=self._URL_PATH, 

644 retry_func=self._needs_retry_for_role_name, 

645 token=token, 

646 ).text 

647 

648 def _get_credentials(self, role_name, token=None): 

649 r = self._get_request( 

650 url_path=self._URL_PATH + role_name, 

651 retry_func=self._needs_retry_for_credentials, 

652 token=token, 

653 ) 

654 return json.loads(r.text) 

655 

656 def _is_invalid_json(self, response): 

657 try: 

658 json.loads(response.text) 

659 return False 

660 except ValueError: 

661 self._log_imds_response(response, 'invalid json') 

662 return True 

663 

664 def _needs_retry_for_role_name(self, response): 

665 return self._is_non_ok_response(response) or self._is_empty(response) 

666 

667 def _needs_retry_for_credentials(self, response): 

668 return ( 

669 self._is_non_ok_response(response) 

670 or self._is_empty(response) 

671 or self._is_invalid_json(response) 

672 ) 

673 

674 def _contains_all_credential_fields(self, credentials): 

675 for field in self._REQUIRED_CREDENTIAL_FIELDS: 

676 if field not in credentials: 

677 logger.debug( 

678 'Retrieved credentials is missing required field: %s', 

679 field, 

680 ) 

681 return False 

682 return True 

683 

684 def _evaluate_expiration(self, credentials): 

685 expiration = credentials.get("expiry_time") 

686 if expiration is None: 

687 return 

688 try: 

689 expiration = datetime.datetime.strptime( 

690 expiration, "%Y-%m-%dT%H:%M:%SZ" 

691 ) 

692 refresh_interval = self._config.get( 

693 "ec2_credential_refresh_window", 60 * 10 

694 ) 

695 jitter = random.randint(120, 600) # Between 2 to 10 minutes 

696 refresh_interval_with_jitter = refresh_interval + jitter 

697 current_time = get_current_datetime() 

698 refresh_offset = datetime.timedelta( 

699 seconds=refresh_interval_with_jitter 

700 ) 

701 extension_time = expiration - refresh_offset 

702 if current_time >= extension_time: 

703 new_time = current_time + refresh_offset 

704 credentials["expiry_time"] = new_time.strftime( 

705 "%Y-%m-%dT%H:%M:%SZ" 

706 ) 

707 logger.info( 

708 "Attempting credential expiration extension due to a " 

709 "credential service availability issue. A refresh of " 

710 "these credentials will be attempted again within " 

711 "the next %.0f minutes.", 

712 refresh_interval_with_jitter / 60, 

713 ) 

714 except ValueError: 

715 logger.debug( 

716 "Unable to parse expiry_time in %s", credentials['expiry_time'] 

717 ) 

718 

719 

720class IMDSRegionProvider: 

721 def __init__(self, session, environ=None, fetcher=None): 

722 """Initialize IMDSRegionProvider. 

723 :type session: :class:`botocore.session.Session` 

724 :param session: The session is needed to look up configuration for 

725 how to contact the instance metadata service. Specifically the 

726 whether or not it should use the IMDS region at all, and if so how 

727 to configure the timeout and number of attempts to reach the 

728 service. 

729 :type environ: None or dict 

730 :param environ: A dictionary of environment variables to use. If 

731 ``None`` is the argument then ``os.environ`` will be used by 

732 default. 

733 :type fecther: :class:`botocore.utils.InstanceMetadataRegionFetcher` 

734 :param fetcher: The class to actually handle the fetching of the region 

735 from the IMDS. If not provided a default one will be created. 

736 """ 

737 self._session = session 

738 if environ is None: 

739 environ = os.environ 

740 self._environ = environ 

741 self._fetcher = fetcher 

742 

743 def provide(self): 

744 """Provide the region value from IMDS.""" 

745 instance_region = self._get_instance_metadata_region() 

746 return instance_region 

747 

748 def _get_instance_metadata_region(self): 

749 fetcher = self._get_fetcher() 

750 region = fetcher.retrieve_region() 

751 return region 

752 

753 def _get_fetcher(self): 

754 if self._fetcher is None: 

755 self._fetcher = self._create_fetcher() 

756 return self._fetcher 

757 

758 def _create_fetcher(self): 

759 metadata_timeout = self._session.get_config_variable( 

760 'metadata_service_timeout' 

761 ) 

762 metadata_num_attempts = self._session.get_config_variable( 

763 'metadata_service_num_attempts' 

764 ) 

765 imds_config = { 

766 'ec2_metadata_service_endpoint': self._session.get_config_variable( 

767 'ec2_metadata_service_endpoint' 

768 ), 

769 'ec2_metadata_service_endpoint_mode': resolve_imds_endpoint_mode( 

770 self._session 

771 ), 

772 'ec2_metadata_v1_disabled': self._session.get_config_variable( 

773 'ec2_metadata_v1_disabled' 

774 ), 

775 } 

776 fetcher = InstanceMetadataRegionFetcher( 

777 timeout=metadata_timeout, 

778 num_attempts=metadata_num_attempts, 

779 env=self._environ, 

780 user_agent=self._session.user_agent(), 

781 config=imds_config, 

782 ) 

783 return fetcher 

784 

785 

786class InstanceMetadataRegionFetcher(IMDSFetcher): 

787 _URL_PATH = 'latest/meta-data/placement/availability-zone/' 

788 

789 def retrieve_region(self): 

790 """Get the current region from the instance metadata service. 

791 :rvalue: str 

792 :returns: The region the current instance is running in or None 

793 if the instance metadata service cannot be contacted or does not 

794 give a valid response. 

795 :rtype: None or str 

796 :returns: Returns the region as a string if it is configured to use 

797 IMDS as a region source. Otherwise returns ``None``. It will also 

798 return ``None`` if it fails to get the region from IMDS due to 

799 exhausting its retries or not being able to connect. 

800 """ 

801 try: 

802 region = self._get_region() 

803 return region 

804 except self._RETRIES_EXCEEDED_ERROR_CLS: 

805 logger.debug( 

806 "Max number of attempts exceeded (%s) when " 

807 "attempting to retrieve data from metadata service.", 

808 self._num_attempts, 

809 ) 

810 return None 

811 

812 def _get_region(self): 

813 token = self._fetch_metadata_token() 

814 response = self._get_request( 

815 url_path=self._URL_PATH, 

816 retry_func=self._default_retry, 

817 token=token, 

818 ) 

819 availability_zone = response.text 

820 region = availability_zone[:-1] 

821 return region 

822 

823 

824def merge_dicts(dict1, dict2, append_lists=False): 

825 """Given two dict, merge the second dict into the first. 

826 

827 The dicts can have arbitrary nesting. 

828 

829 :param append_lists: If true, instead of clobbering a list with the new 

830 value, append all of the new values onto the original list. 

831 """ 

832 for key in dict2: 

833 if isinstance(dict2[key], dict): 

834 if key in dict1 and key in dict2: 

835 merge_dicts(dict1[key], dict2[key]) 

836 else: 

837 dict1[key] = dict2[key] 

838 # If the value is a list and the ``append_lists`` flag is set, 

839 # append the new values onto the original list 

840 elif isinstance(dict2[key], list) and append_lists: 

841 # The value in dict1 must be a list in order to append new 

842 # values onto it. 

843 if key in dict1 and isinstance(dict1[key], list): 

844 dict1[key].extend(dict2[key]) 

845 else: 

846 dict1[key] = dict2[key] 

847 else: 

848 # At scalar types, we iterate and merge the 

849 # current dict that we're on. 

850 dict1[key] = dict2[key] 

851 

852 

853def lowercase_dict(original): 

854 """Copies the given dictionary ensuring all keys are lowercase strings.""" 

855 copy = {} 

856 for key in original: 

857 copy[key.lower()] = original[key] 

858 return copy 

859 

860 

861def parse_key_val_file(filename, _open=open): 

862 try: 

863 with _open(filename) as f: 

864 contents = f.read() 

865 return parse_key_val_file_contents(contents) 

866 except OSError: 

867 raise ConfigNotFound(path=filename) 

868 

869 

870def parse_key_val_file_contents(contents): 

871 # This was originally extracted from the EC2 credential provider, which was 

872 # fairly lenient in its parsing. We only try to parse key/val pairs if 

873 # there's a '=' in the line. 

874 final = {} 

875 for line in contents.splitlines(): 

876 if '=' not in line: 

877 continue 

878 key, val = line.split('=', 1) 

879 key = key.strip() 

880 val = val.strip() 

881 final[key] = val 

882 return final 

883 

884 

885def percent_encode_sequence(mapping, safe=SAFE_CHARS): 

886 """Urlencode a dict or list into a string. 

887 

888 This is similar to urllib.urlencode except that: 

889 

890 * It uses quote, and not quote_plus 

891 * It has a default list of safe chars that don't need 

892 to be encoded, which matches what AWS services expect. 

893 

894 If any value in the input ``mapping`` is a list type, 

895 then each list element wil be serialized. This is the equivalent 

896 to ``urlencode``'s ``doseq=True`` argument. 

897 

898 This function should be preferred over the stdlib 

899 ``urlencode()`` function. 

900 

901 :param mapping: Either a dict to urlencode or a list of 

902 ``(key, value)`` pairs. 

903 

904 """ 

905 encoded_pairs = [] 

906 if hasattr(mapping, 'items'): 

907 pairs = mapping.items() 

908 else: 

909 pairs = mapping 

910 for key, value in pairs: 

911 if isinstance(value, list): 

912 for element in value: 

913 encoded_pairs.append( 

914 f'{percent_encode(key)}={percent_encode(element)}' 

915 ) 

916 else: 

917 encoded_pairs.append( 

918 f'{percent_encode(key)}={percent_encode(value)}' 

919 ) 

920 return '&'.join(encoded_pairs) 

921 

922 

923def percent_encode(input_str, safe=SAFE_CHARS): 

924 """Urlencodes a string. 

925 

926 Whereas percent_encode_sequence handles taking a dict/sequence and 

927 producing a percent encoded string, this function deals only with 

928 taking a string (not a dict/sequence) and percent encoding it. 

929 

930 If given the binary type, will simply URL encode it. If given the 

931 text type, will produce the binary type by UTF-8 encoding the 

932 text. If given something else, will convert it to the text type 

933 first. 

934 """ 

935 # If its not a binary or text string, make it a text string. 

936 if not isinstance(input_str, (bytes, str)): 

937 input_str = str(input_str) 

938 # If it's not bytes, make it bytes by UTF-8 encoding it. 

939 if not isinstance(input_str, bytes): 

940 input_str = input_str.encode('utf-8') 

941 return quote(input_str, safe=safe) 

942 

943 

944def _epoch_seconds_to_datetime(value, tzinfo): 

945 """Parse numerical epoch timestamps (seconds since 1970) into a 

946 ``datetime.datetime`` in UTC using ``datetime.timedelta``. This is intended 

947 as fallback when ``fromtimestamp`` raises ``OverflowError`` or ``OSError``. 

948 

949 :type value: float or int 

950 :param value: The Unix timestamps as number. 

951 

952 :type tzinfo: callable 

953 :param tzinfo: A ``datetime.tzinfo`` class or compatible callable. 

954 """ 

955 epoch_zero = datetime.datetime(1970, 1, 1, 0, 0, 0, tzinfo=tzutc()) 

956 epoch_zero_localized = epoch_zero.astimezone(tzinfo()) 

957 return epoch_zero_localized + datetime.timedelta(seconds=value) 

958 

959 

960def _parse_timestamp_with_tzinfo(value, tzinfo): 

961 """Parse timestamp with pluggable tzinfo options.""" 

962 if isinstance(value, (int, float)): 

963 # Possibly an epoch time. 

964 return datetime.datetime.fromtimestamp(value, tzinfo()) 

965 else: 

966 try: 

967 return datetime.datetime.fromtimestamp(float(value), tzinfo()) 

968 except (TypeError, ValueError): 

969 pass 

970 try: 

971 # In certain cases, a timestamp marked with GMT can be parsed into a 

972 # different time zone, so here we provide a context which will 

973 # enforce that GMT == UTC. 

974 return dateutil.parser.parse(value, tzinfos={'GMT': tzutc()}) 

975 except (TypeError, ValueError) as e: 

976 raise ValueError(f'Invalid timestamp "{value}": {e}') 

977 

978 

979def parse_timestamp(value): 

980 """Parse a timestamp into a datetime object. 

981 

982 Supported formats: 

983 

984 * iso8601 

985 * rfc822 

986 * epoch (value is an integer) 

987 

988 This will return a ``datetime.datetime`` object. 

989 

990 """ 

991 tzinfo_options = get_tzinfo_options() 

992 for tzinfo in tzinfo_options: 

993 try: 

994 return _parse_timestamp_with_tzinfo(value, tzinfo) 

995 except (OSError, OverflowError) as e: 

996 logger.debug( 

997 'Unable to parse timestamp with "%s" timezone info.', 

998 tzinfo.__name__, 

999 exc_info=e, 

1000 ) 

1001 # For numeric values attempt fallback to using fromtimestamp-free method. 

1002 # From Python's ``datetime.datetime.fromtimestamp`` documentation: "This 

1003 # may raise ``OverflowError``, if the timestamp is out of the range of 

1004 # values supported by the platform C localtime() function, and ``OSError`` 

1005 # on localtime() failure. It's common for this to be restricted to years 

1006 # from 1970 through 2038." 

1007 try: 

1008 numeric_value = float(value) 

1009 except (TypeError, ValueError): 

1010 pass 

1011 else: 

1012 try: 

1013 for tzinfo in tzinfo_options: 

1014 return _epoch_seconds_to_datetime(numeric_value, tzinfo=tzinfo) 

1015 except (OSError, OverflowError) as e: 

1016 logger.debug( 

1017 'Unable to parse timestamp using fallback method with "%s" ' 

1018 'timezone info.', 

1019 tzinfo.__name__, 

1020 exc_info=e, 

1021 ) 

1022 raise RuntimeError( 

1023 f'Unable to calculate correct timezone offset for "{value}"' 

1024 ) 

1025 

1026 

1027def parse_to_aware_datetime(value): 

1028 """Converted the passed in value to a datetime object with tzinfo. 

1029 

1030 This function can be used to normalize all timestamp inputs. This 

1031 function accepts a number of different types of inputs, but 

1032 will always return a datetime.datetime object with time zone 

1033 information. 

1034 

1035 The input param ``value`` can be one of several types: 

1036 

1037 * A datetime object (both naive and aware) 

1038 * An integer representing the epoch time (can also be a string 

1039 of the integer, i.e '0', instead of 0). The epoch time is 

1040 considered to be UTC. 

1041 * An iso8601 formatted timestamp. This does not need to be 

1042 a complete timestamp, it can contain just the date portion 

1043 without the time component. 

1044 

1045 The returned value will be a datetime object that will have tzinfo. 

1046 If no timezone info was provided in the input value, then UTC is 

1047 assumed, not local time. 

1048 

1049 """ 

1050 # This is a general purpose method that handles several cases of 

1051 # converting the provided value to a string timestamp suitable to be 

1052 # serialized to an http request. It can handle: 

1053 # 1) A datetime.datetime object. 

1054 if isinstance(value, _DatetimeClass): 

1055 datetime_obj = value 

1056 else: 

1057 # 2) A string object that's formatted as a timestamp. 

1058 # We document this as being an iso8601 timestamp, although 

1059 # parse_timestamp is a bit more flexible. 

1060 datetime_obj = parse_timestamp(value) 

1061 if datetime_obj.tzinfo is None: 

1062 # I think a case would be made that if no time zone is provided, 

1063 # we should use the local time. However, to restore backwards 

1064 # compat, the previous behavior was to assume UTC, which is 

1065 # what we're going to do here. 

1066 datetime_obj = datetime_obj.replace(tzinfo=tzutc()) 

1067 else: 

1068 datetime_obj = datetime_obj.astimezone(tzutc()) 

1069 return datetime_obj 

1070 

1071 

1072def datetime2timestamp(dt, default_timezone=None): 

1073 """Calculate the timestamp based on the given datetime instance. 

1074 

1075 :type dt: datetime 

1076 :param dt: A datetime object to be converted into timestamp 

1077 :type default_timezone: tzinfo 

1078 :param default_timezone: If it is provided as None, we treat it as tzutc(). 

1079 But it is only used when dt is a naive datetime. 

1080 :returns: The timestamp 

1081 """ 

1082 epoch = datetime.datetime(1970, 1, 1) 

1083 if dt.tzinfo is None: 

1084 if default_timezone is None: 

1085 default_timezone = tzutc() 

1086 dt = dt.replace(tzinfo=default_timezone) 

1087 d = dt.replace(tzinfo=None) - dt.utcoffset() - epoch 

1088 return d.total_seconds() 

1089 

1090 

1091def calculate_sha256(body, as_hex=False): 

1092 """Calculate a sha256 checksum. 

1093 

1094 This method will calculate the sha256 checksum of a file like 

1095 object. Note that this method will iterate through the entire 

1096 file contents. The caller is responsible for ensuring the proper 

1097 starting position of the file and ``seek()``'ing the file back 

1098 to its starting location if other consumers need to read from 

1099 the file like object. 

1100 

1101 :param body: Any file like object. The file must be opened 

1102 in binary mode such that a ``.read()`` call returns bytes. 

1103 :param as_hex: If True, then the hex digest is returned. 

1104 If False, then the digest (as binary bytes) is returned. 

1105 

1106 :returns: The sha256 checksum 

1107 

1108 """ 

1109 checksum = hashlib.sha256() 

1110 for chunk in iter(lambda: body.read(1024 * 1024), b''): 

1111 checksum.update(chunk) 

1112 if as_hex: 

1113 return checksum.hexdigest() 

1114 else: 

1115 return checksum.digest() 

1116 

1117 

1118def calculate_tree_hash(body): 

1119 """Calculate a tree hash checksum. 

1120 

1121 For more information see: 

1122 

1123 http://docs.aws.amazon.com/amazonglacier/latest/dev/checksum-calculations.html 

1124 

1125 :param body: Any file like object. This has the same constraints as 

1126 the ``body`` param in calculate_sha256 

1127 

1128 :rtype: str 

1129 :returns: The hex version of the calculated tree hash 

1130 

1131 """ 

1132 chunks = [] 

1133 required_chunk_size = 1024 * 1024 

1134 sha256 = hashlib.sha256 

1135 for chunk in iter(lambda: body.read(required_chunk_size), b''): 

1136 chunks.append(sha256(chunk).digest()) 

1137 if not chunks: 

1138 return sha256(b'').hexdigest() 

1139 while len(chunks) > 1: 

1140 new_chunks = [] 

1141 for first, second in _in_pairs(chunks): 

1142 if second is not None: 

1143 new_chunks.append(sha256(first + second).digest()) 

1144 else: 

1145 # We're at the end of the list and there's no pair left. 

1146 new_chunks.append(first) 

1147 chunks = new_chunks 

1148 return binascii.hexlify(chunks[0]).decode('ascii') 

1149 

1150 

1151def _in_pairs(iterable): 

1152 # Creates iterator that iterates over the list in pairs: 

1153 # for a, b in _in_pairs([0, 1, 2, 3, 4]): 

1154 # print(a, b) 

1155 # 

1156 # will print: 

1157 # 0, 1 

1158 # 2, 3 

1159 # 4, None 

1160 shared_iter = iter(iterable) 

1161 # Note that zip_longest is a compat import that uses 

1162 # the itertools izip_longest. This creates an iterator, 

1163 # this call below does _not_ immediately create the list 

1164 # of pairs. 

1165 return zip_longest(shared_iter, shared_iter) 

1166 

1167 

1168class CachedProperty: 

1169 """A read only property that caches the initially computed value. 

1170 

1171 This descriptor will only call the provided ``fget`` function once. 

1172 Subsequent access to this property will return the cached value. 

1173 

1174 """ 

1175 

1176 def __init__(self, fget): 

1177 self._fget = fget 

1178 

1179 def __get__(self, obj, cls): 

1180 if obj is None: 

1181 return self 

1182 else: 

1183 computed_value = self._fget(obj) 

1184 obj.__dict__[self._fget.__name__] = computed_value 

1185 return computed_value 

1186 

1187 

1188class ArgumentGenerator: 

1189 """Generate sample input based on a shape model. 

1190 

1191 This class contains a ``generate_skeleton`` method that will take 

1192 an input/output shape (created from ``botocore.model``) and generate 

1193 a sample dictionary corresponding to the input/output shape. 

1194 

1195 The specific values used are place holder values. For strings either an 

1196 empty string or the member name can be used, for numbers 0 or 0.0 is used. 

1197 The intended usage of this class is to generate the *shape* of the input 

1198 structure. 

1199 

1200 This can be useful for operations that have complex input shapes. 

1201 This allows a user to just fill in the necessary data instead of 

1202 worrying about the specific structure of the input arguments. 

1203 

1204 Example usage:: 

1205 

1206 s = botocore.session.get_session() 

1207 ddb = s.get_service_model('dynamodb') 

1208 arg_gen = ArgumentGenerator() 

1209 sample_input = arg_gen.generate_skeleton( 

1210 ddb.operation_model('CreateTable').input_shape) 

1211 print("Sample input for dynamodb.CreateTable: %s" % sample_input) 

1212 

1213 """ 

1214 

1215 def __init__(self, use_member_names=False): 

1216 self._use_member_names = use_member_names 

1217 

1218 def generate_skeleton(self, shape): 

1219 """Generate a sample input. 

1220 

1221 :type shape: ``botocore.model.Shape`` 

1222 :param shape: The input shape. 

1223 

1224 :return: The generated skeleton input corresponding to the 

1225 provided input shape. 

1226 

1227 """ 

1228 stack = [] 

1229 return self._generate_skeleton(shape, stack) 

1230 

1231 def _generate_skeleton(self, shape, stack, name=''): 

1232 stack.append(shape.name) 

1233 try: 

1234 if shape.type_name == 'structure': 

1235 return self._generate_type_structure(shape, stack) 

1236 elif shape.type_name == 'list': 

1237 return self._generate_type_list(shape, stack) 

1238 elif shape.type_name == 'map': 

1239 return self._generate_type_map(shape, stack) 

1240 elif shape.type_name == 'string': 

1241 if self._use_member_names: 

1242 return name 

1243 if shape.enum: 

1244 return random.choice(shape.enum) 

1245 return '' 

1246 elif shape.type_name in ['integer', 'long']: 

1247 return 0 

1248 elif shape.type_name in ['float', 'double']: 

1249 return 0.0 

1250 elif shape.type_name == 'boolean': 

1251 return True 

1252 elif shape.type_name == 'timestamp': 

1253 return datetime.datetime(1970, 1, 1, 0, 0, 0) 

1254 finally: 

1255 stack.pop() 

1256 

1257 def _generate_type_structure(self, shape, stack): 

1258 if stack.count(shape.name) > 1: 

1259 return {} 

1260 skeleton = OrderedDict() 

1261 for member_name, member_shape in shape.members.items(): 

1262 skeleton[member_name] = self._generate_skeleton( 

1263 member_shape, stack, name=member_name 

1264 ) 

1265 return skeleton 

1266 

1267 def _generate_type_list(self, shape, stack): 

1268 # For list elements we've arbitrarily decided to 

1269 # return two elements for the skeleton list. 

1270 name = '' 

1271 if self._use_member_names: 

1272 name = shape.member.name 

1273 return [ 

1274 self._generate_skeleton(shape.member, stack, name), 

1275 ] 

1276 

1277 def _generate_type_map(self, shape, stack): 

1278 key_shape = shape.key 

1279 value_shape = shape.value 

1280 assert key_shape.type_name == 'string' 

1281 return OrderedDict( 

1282 [ 

1283 ('KeyName', self._generate_skeleton(value_shape, stack)), 

1284 ] 

1285 ) 

1286 

1287 

1288def is_valid_ipv6_endpoint_url(endpoint_url): 

1289 if UNSAFE_URL_CHARS.intersection(endpoint_url): 

1290 return False 

1291 hostname = f'[{urlparse(endpoint_url).hostname}]' 

1292 return IPV6_ADDRZ_RE.match(hostname) is not None 

1293 

1294 

1295def is_valid_ipv4_endpoint_url(endpoint_url): 

1296 hostname = urlparse(endpoint_url).hostname 

1297 return IPV4_RE.match(hostname) is not None 

1298 

1299 

1300def is_valid_endpoint_url(endpoint_url): 

1301 """Verify the endpoint_url is valid. 

1302 

1303 :type endpoint_url: string 

1304 :param endpoint_url: An endpoint_url. Must have at least a scheme 

1305 and a hostname. 

1306 

1307 :return: True if the endpoint url is valid. False otherwise. 

1308 

1309 """ 

1310 # post-bpo-43882 urlsplit() strips unsafe characters from URL, causing 

1311 # it to pass hostname validation below. Detect them early to fix that. 

1312 if UNSAFE_URL_CHARS.intersection(endpoint_url): 

1313 return False 

1314 parts = urlsplit(endpoint_url) 

1315 hostname = parts.hostname 

1316 if hostname is None: 

1317 return False 

1318 if len(hostname) > 255: 

1319 return False 

1320 if hostname[-1] == ".": 

1321 hostname = hostname[:-1] 

1322 allowed = re.compile( 

1323 r"^((?!-)[A-Z\d-]{1,63}(?<!-)\.)*((?!-)[A-Z\d-]{1,63}(?<!-))$", 

1324 re.IGNORECASE, 

1325 ) 

1326 return allowed.match(hostname) 

1327 

1328 

1329def is_valid_uri(endpoint_url): 

1330 return is_valid_endpoint_url(endpoint_url) or is_valid_ipv6_endpoint_url( 

1331 endpoint_url 

1332 ) 

1333 

1334 

1335def validate_region_name(region_name): 

1336 """Provided region_name must be a valid host label.""" 

1337 if region_name is None: 

1338 return 

1339 valid_host_label = re.compile(r'^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{,63}(?<!-)$') 

1340 valid = valid_host_label.match(region_name) 

1341 if not valid: 

1342 raise InvalidRegionError(region_name=region_name) 

1343 

1344 

1345def check_dns_name(bucket_name): 

1346 """ 

1347 Check to see if the ``bucket_name`` complies with the 

1348 restricted DNS naming conventions necessary to allow 

1349 access via virtual-hosting style. 

1350 

1351 Even though "." characters are perfectly valid in this DNS 

1352 naming scheme, we are going to punt on any name containing a 

1353 "." character because these will cause SSL cert validation 

1354 problems if we try to use virtual-hosting style addressing. 

1355 """ 

1356 if '.' in bucket_name: 

1357 return False 

1358 n = len(bucket_name) 

1359 if n < 3 or n > 63: 

1360 # Wrong length 

1361 return False 

1362 match = LABEL_RE.match(bucket_name) 

1363 if match is None or match.end() != len(bucket_name): 

1364 return False 

1365 return True 

1366 

1367 

1368def fix_s3_host( 

1369 request, 

1370 signature_version, 

1371 region_name, 

1372 default_endpoint_url=None, 

1373 **kwargs, 

1374): 

1375 """ 

1376 This handler looks at S3 requests just before they are signed. 

1377 If there is a bucket name on the path (true for everything except 

1378 ListAllBuckets) it checks to see if that bucket name conforms to 

1379 the DNS naming conventions. If it does, it alters the request to 

1380 use ``virtual hosting`` style addressing rather than ``path-style`` 

1381 addressing. 

1382 

1383 """ 

1384 if request.context.get('use_global_endpoint', False): 

1385 default_endpoint_url = 's3.amazonaws.com' 

1386 try: 

1387 switch_to_virtual_host_style( 

1388 request, signature_version, default_endpoint_url 

1389 ) 

1390 except InvalidDNSNameError as e: 

1391 bucket_name = e.kwargs['bucket_name'] 

1392 logger.debug( 

1393 'Not changing URI, bucket is not DNS compatible: %s', bucket_name 

1394 ) 

1395 

1396 

1397def switch_to_virtual_host_style( 

1398 request, signature_version, default_endpoint_url=None, **kwargs 

1399): 

1400 """ 

1401 This is a handler to force virtual host style s3 addressing no matter 

1402 the signature version (which is taken in consideration for the default 

1403 case). If the bucket is not DNS compatible an InvalidDNSName is thrown. 

1404 

1405 :param request: A AWSRequest object that is about to be sent. 

1406 :param signature_version: The signature version to sign with 

1407 :param default_endpoint_url: The endpoint to use when switching to a 

1408 virtual style. If None is supplied, the virtual host will be 

1409 constructed from the url of the request. 

1410 """ 

1411 if request.auth_path is not None: 

1412 # The auth_path has already been applied (this may be a 

1413 # retried request). We don't need to perform this 

1414 # customization again. 

1415 return 

1416 elif _is_get_bucket_location_request(request): 

1417 # For the GetBucketLocation response, we should not be using 

1418 # the virtual host style addressing so we can avoid any sigv4 

1419 # issues. 

1420 logger.debug( 

1421 "Request is GetBucketLocation operation, not checking " 

1422 "for DNS compatibility." 

1423 ) 

1424 return 

1425 parts = urlsplit(request.url) 

1426 request.auth_path = parts.path 

1427 path_parts = parts.path.split('/') 

1428 

1429 # Retrieve what the endpoint we will be prepending the bucket name to. 

1430 if default_endpoint_url is None: 

1431 default_endpoint_url = parts.netloc 

1432 

1433 if len(path_parts) > 1: 

1434 bucket_name = path_parts[1] 

1435 if not bucket_name: 

1436 # If the bucket name is empty we should not be checking for 

1437 # dns compatibility. 

1438 return 

1439 logger.debug('Checking for DNS compatible bucket for: %s', request.url) 

1440 if check_dns_name(bucket_name): 

1441 # If the operation is on a bucket, the auth_path must be 

1442 # terminated with a '/' character. 

1443 if len(path_parts) == 2: 

1444 if request.auth_path[-1] != '/': 

1445 request.auth_path += '/' 

1446 path_parts.remove(bucket_name) 

1447 # At the very least the path must be a '/', such as with the 

1448 # CreateBucket operation when DNS style is being used. If this 

1449 # is not used you will get an empty path which is incorrect. 

1450 path = '/'.join(path_parts) or '/' 

1451 global_endpoint = default_endpoint_url 

1452 host = bucket_name + '.' + global_endpoint 

1453 new_tuple = (parts.scheme, host, path, parts.query, '') 

1454 new_uri = urlunsplit(new_tuple) 

1455 request.url = new_uri 

1456 logger.debug('URI updated to: %s', new_uri) 

1457 else: 

1458 raise InvalidDNSNameError(bucket_name=bucket_name) 

1459 

1460 

1461def _is_get_bucket_location_request(request): 

1462 return request.url.endswith('?location') 

1463 

1464 

1465def instance_cache(func): 

1466 """Method decorator for caching method calls to a single instance. 

1467 

1468 **This is not a general purpose caching decorator.** 

1469 

1470 In order to use this, you *must* provide an ``_instance_cache`` 

1471 attribute on the instance. 

1472 

1473 This decorator is used to cache method calls. The cache is only 

1474 scoped to a single instance though such that multiple instances 

1475 will maintain their own cache. In order to keep things simple, 

1476 this decorator requires that you provide an ``_instance_cache`` 

1477 attribute on your instance. 

1478 

1479 """ 

1480 func_name = func.__name__ 

1481 

1482 @functools.wraps(func) 

1483 def _cache_guard(self, *args, **kwargs): 

1484 cache_key = (func_name, args) 

1485 if kwargs: 

1486 kwarg_items = tuple(sorted(kwargs.items())) 

1487 cache_key = (func_name, args, kwarg_items) 

1488 result = self._instance_cache.get(cache_key) 

1489 if result is not None: 

1490 return result 

1491 result = func(self, *args, **kwargs) 

1492 self._instance_cache[cache_key] = result 

1493 return result 

1494 

1495 return _cache_guard 

1496 

1497 

1498def lru_cache_weakref(*cache_args, **cache_kwargs): 

1499 """ 

1500 Version of functools.lru_cache that stores a weak reference to ``self``. 

1501 

1502 Serves the same purpose as :py:func:`instance_cache` but uses Python's 

1503 functools implementation which offers ``max_size`` and ``typed`` properties. 

1504 

1505 lru_cache is a global cache even when used on a method. The cache's 

1506 reference to ``self`` will prevent garbage collection of the object. This 

1507 wrapper around functools.lru_cache replaces the reference to ``self`` with 

1508 a weak reference to not interfere with garbage collection. 

1509 """ 

1510 

1511 def wrapper(func): 

1512 @functools.lru_cache(*cache_args, **cache_kwargs) 

1513 def func_with_weakref(weakref_to_self, *args, **kwargs): 

1514 return func(weakref_to_self(), *args, **kwargs) 

1515 

1516 @functools.wraps(func) 

1517 def inner(self, *args, **kwargs): 

1518 for kwarg_key, kwarg_value in kwargs.items(): 

1519 if isinstance(kwarg_value, list): 

1520 kwargs[kwarg_key] = tuple(kwarg_value) 

1521 return func_with_weakref(weakref.ref(self), *args, **kwargs) 

1522 

1523 inner.cache_info = func_with_weakref.cache_info 

1524 return inner 

1525 

1526 return wrapper 

1527 

1528 

1529def switch_host_s3_accelerate(request, operation_name, **kwargs): 

1530 """Switches the current s3 endpoint with an S3 Accelerate endpoint""" 

1531 

1532 # Note that when registered the switching of the s3 host happens 

1533 # before it gets changed to virtual. So we are not concerned with ensuring 

1534 # that the bucket name is translated to the virtual style here and we 

1535 # can hard code the Accelerate endpoint. 

1536 parts = urlsplit(request.url).netloc.split('.') 

1537 parts = [p for p in parts if p in S3_ACCELERATE_WHITELIST] 

1538 endpoint = 'https://s3-accelerate.' 

1539 if len(parts) > 0: 

1540 endpoint += '.'.join(parts) + '.' 

1541 endpoint += 'amazonaws.com' 

1542 

1543 if operation_name in ['ListBuckets', 'CreateBucket', 'DeleteBucket']: 

1544 return 

1545 _switch_hosts(request, endpoint, use_new_scheme=False) 

1546 

1547 

1548def switch_host_with_param(request, param_name): 

1549 """Switches the host using a parameter value from a JSON request body""" 

1550 request_json = json.loads(request.data.decode('utf-8')) 

1551 if request_json.get(param_name): 

1552 new_endpoint = request_json[param_name] 

1553 _switch_hosts(request, new_endpoint) 

1554 

1555 

1556def _switch_hosts(request, new_endpoint, use_new_scheme=True): 

1557 final_endpoint = _get_new_endpoint( 

1558 request.url, new_endpoint, use_new_scheme 

1559 ) 

1560 request.url = final_endpoint 

1561 

1562 

1563def _get_new_endpoint(original_endpoint, new_endpoint, use_new_scheme=True): 

1564 new_endpoint_components = urlsplit(new_endpoint) 

1565 original_endpoint_components = urlsplit(original_endpoint) 

1566 scheme = original_endpoint_components.scheme 

1567 if use_new_scheme: 

1568 scheme = new_endpoint_components.scheme 

1569 final_endpoint_components = ( 

1570 scheme, 

1571 new_endpoint_components.netloc, 

1572 original_endpoint_components.path, 

1573 original_endpoint_components.query, 

1574 '', 

1575 ) 

1576 final_endpoint = urlunsplit(final_endpoint_components) 

1577 logger.debug( 

1578 'Updating URI from %s to %s', original_endpoint, final_endpoint 

1579 ) 

1580 return final_endpoint 

1581 

1582 

1583def deep_merge(base, extra): 

1584 """Deeply two dictionaries, overriding existing keys in the base. 

1585 

1586 :param base: The base dictionary which will be merged into. 

1587 :param extra: The dictionary to merge into the base. Keys from this 

1588 dictionary will take precedence. 

1589 """ 

1590 for key in extra: 

1591 # If the key represents a dict on both given dicts, merge the sub-dicts 

1592 if ( 

1593 key in base 

1594 and isinstance(base[key], dict) 

1595 and isinstance(extra[key], dict) 

1596 ): 

1597 deep_merge(base[key], extra[key]) 

1598 continue 

1599 

1600 # Otherwise, set the key on the base to be the value of the extra. 

1601 base[key] = extra[key] 

1602 

1603 

1604def hyphenize_service_id(service_id): 

1605 """Translate the form used for event emitters. 

1606 

1607 :param service_id: The service_id to convert. 

1608 """ 

1609 return service_id.replace(' ', '-').lower() 

1610 

1611 

1612class IdentityCache: 

1613 """Base IdentityCache implementation for storing and retrieving 

1614 highly accessed credentials. 

1615 

1616 This class is not intended to be instantiated in user code. 

1617 """ 

1618 

1619 METHOD = "base_identity_cache" 

1620 

1621 def __init__(self, client, credential_cls): 

1622 self._client = client 

1623 self._credential_cls = credential_cls 

1624 

1625 def get_credentials(self, **kwargs): 

1626 callback = self.build_refresh_callback(**kwargs) 

1627 metadata = callback() 

1628 credential_entry = self._credential_cls.create_from_metadata( 

1629 metadata=metadata, 

1630 refresh_using=callback, 

1631 method=self.METHOD, 

1632 advisory_timeout=45, 

1633 mandatory_timeout=10, 

1634 ) 

1635 return credential_entry 

1636 

1637 def build_refresh_callback(**kwargs): 

1638 """Callback to be implemented by subclasses. 

1639 

1640 Returns a set of metadata to be converted into a new 

1641 credential instance. 

1642 """ 

1643 raise NotImplementedError() 

1644 

1645 

1646class S3ExpressIdentityCache(IdentityCache): 

1647 """S3Express IdentityCache for retrieving and storing 

1648 credentials from CreateSession calls. 

1649 

1650 This class is not intended to be instantiated in user code. 

1651 """ 

1652 

1653 METHOD = "s3express" 

1654 

1655 def __init__(self, client, credential_cls): 

1656 self._client = client 

1657 self._credential_cls = credential_cls 

1658 

1659 @functools.lru_cache(maxsize=100) 

1660 def get_credentials(self, bucket): 

1661 return super().get_credentials(bucket=bucket) 

1662 

1663 def build_refresh_callback(self, bucket): 

1664 def refresher(): 

1665 response = self._client.create_session(Bucket=bucket) 

1666 creds = response['Credentials'] 

1667 expiration = self._serialize_if_needed( 

1668 creds['Expiration'], iso=True 

1669 ) 

1670 return { 

1671 "access_key": creds['AccessKeyId'], 

1672 "secret_key": creds['SecretAccessKey'], 

1673 "token": creds['SessionToken'], 

1674 "expiry_time": expiration, 

1675 } 

1676 

1677 return refresher 

1678 

1679 def _serialize_if_needed(self, value, iso=False): 

1680 if isinstance(value, _DatetimeClass): 

1681 if iso: 

1682 return value.isoformat() 

1683 return value.strftime('%Y-%m-%dT%H:%M:%S%Z') 

1684 return value 

1685 

1686 

1687class S3ExpressIdentityResolver: 

1688 def __init__(self, client, credential_cls, cache=None): 

1689 self._client = weakref.proxy(client) 

1690 

1691 if cache is None: 

1692 cache = S3ExpressIdentityCache(self._client, credential_cls) 

1693 self._cache = cache 

1694 

1695 def register(self, event_emitter=None): 

1696 logger.debug('Registering S3Express Identity Resolver') 

1697 emitter = event_emitter or self._client.meta.events 

1698 emitter.register('before-call.s3', self.apply_signing_cache_key) 

1699 emitter.register('before-sign.s3', self.resolve_s3express_identity) 

1700 

1701 def apply_signing_cache_key(self, params, context, **kwargs): 

1702 endpoint_properties = context.get('endpoint_properties', {}) 

1703 backend = endpoint_properties.get('backend', None) 

1704 

1705 # Add cache key if Bucket supplied for s3express request 

1706 bucket_name = context.get('input_params', {}).get('Bucket') 

1707 if backend == 'S3Express' and bucket_name is not None: 

1708 context.setdefault('signing', {}) 

1709 context['signing']['cache_key'] = bucket_name 

1710 

1711 def resolve_s3express_identity( 

1712 self, 

1713 request, 

1714 signing_name, 

1715 region_name, 

1716 signature_version, 

1717 request_signer, 

1718 operation_name, 

1719 **kwargs, 

1720 ): 

1721 signing_context = request.context.get('signing', {}) 

1722 signing_name = signing_context.get('signing_name') 

1723 if signing_name == 's3express' and signature_version.startswith( 

1724 'v4-s3express' 

1725 ): 

1726 signing_context['identity_cache'] = self._cache 

1727 if 'cache_key' not in signing_context: 

1728 signing_context['cache_key'] = ( 

1729 request.context.get('s3_redirect', {}) 

1730 .get('params', {}) 

1731 .get('Bucket') 

1732 ) 

1733 

1734 

1735class S3RegionRedirectorv2: 

1736 """Updated version of S3RegionRedirector for use when 

1737 EndpointRulesetResolver is in use for endpoint resolution. 

1738 

1739 This class is considered private and subject to abrupt breaking changes or 

1740 removal without prior announcement. Please do not use it directly. 

1741 """ 

1742 

1743 def __init__(self, endpoint_bridge, client, cache=None): 

1744 self._cache = cache or {} 

1745 self._client = weakref.proxy(client) 

1746 

1747 def register(self, event_emitter=None): 

1748 logger.debug('Registering S3 region redirector handler') 

1749 emitter = event_emitter or self._client.meta.events 

1750 emitter.register('needs-retry.s3', self.redirect_from_error) 

1751 emitter.register( 

1752 'before-parameter-build.s3', self.annotate_request_context 

1753 ) 

1754 emitter.register( 

1755 'before-endpoint-resolution.s3', self.redirect_from_cache 

1756 ) 

1757 

1758 def redirect_from_error(self, request_dict, response, operation, **kwargs): 

1759 """ 

1760 An S3 request sent to the wrong region will return an error that 

1761 contains the endpoint the request should be sent to. This handler 

1762 will add the redirect information to the signing context and then 

1763 redirect the request. 

1764 """ 

1765 if response is None: 

1766 # This could be none if there was a ConnectionError or other 

1767 # transport error. 

1768 return 

1769 

1770 redirect_ctx = request_dict.get('context', {}).get('s3_redirect', {}) 

1771 if ArnParser.is_arn(redirect_ctx.get('bucket')): 

1772 logger.debug( 

1773 'S3 request was previously for an Accesspoint ARN, not ' 

1774 'redirecting.' 

1775 ) 

1776 return 

1777 

1778 if redirect_ctx.get('redirected'): 

1779 logger.debug( 

1780 'S3 request was previously redirected, not redirecting.' 

1781 ) 

1782 return 

1783 

1784 error = response[1].get('Error', {}) 

1785 error_code = error.get('Code') 

1786 response_metadata = response[1].get('ResponseMetadata', {}) 

1787 

1788 # We have to account for 400 responses because 

1789 # if we sign a Head* request with the wrong region, 

1790 # we'll get a 400 Bad Request but we won't get a 

1791 # body saying it's an "AuthorizationHeaderMalformed". 

1792 is_special_head_object = ( 

1793 error_code in ('301', '400') and operation.name == 'HeadObject' 

1794 ) 

1795 is_special_head_bucket = ( 

1796 error_code in ('301', '400') 

1797 and operation.name == 'HeadBucket' 

1798 and 'x-amz-bucket-region' 

1799 in response_metadata.get('HTTPHeaders', {}) 

1800 ) 

1801 is_wrong_signing_region = ( 

1802 error_code == 'AuthorizationHeaderMalformed' and 'Region' in error 

1803 ) 

1804 is_redirect_status = response[0] is not None and response[ 

1805 0 

1806 ].status_code in (301, 302, 307) 

1807 is_permanent_redirect = error_code == 'PermanentRedirect' 

1808 is_opt_in_region_redirect = ( 

1809 error_code == 'IllegalLocationConstraintException' 

1810 and operation.name != 'CreateBucket' 

1811 ) 

1812 if not any( 

1813 [ 

1814 is_special_head_object, 

1815 is_wrong_signing_region, 

1816 is_permanent_redirect, 

1817 is_special_head_bucket, 

1818 is_redirect_status, 

1819 is_opt_in_region_redirect, 

1820 ] 

1821 ): 

1822 return 

1823 

1824 bucket = request_dict['context']['s3_redirect']['bucket'] 

1825 client_region = request_dict['context'].get('client_region') 

1826 new_region = self.get_bucket_region(bucket, response) 

1827 

1828 if new_region is None: 

1829 logger.debug( 

1830 "S3 client configured for region %s but the " 

1831 "bucket %s is not in that region and the proper region " 

1832 "could not be automatically determined.", 

1833 client_region, 

1834 bucket, 

1835 ) 

1836 return 

1837 

1838 logger.debug( 

1839 "S3 client configured for region %s but the bucket %s " 

1840 "is in region %s; Please configure the proper region to " 

1841 "avoid multiple unnecessary redirects and signing attempts.", 

1842 client_region, 

1843 bucket, 

1844 new_region, 

1845 ) 

1846 register_feature_id('S3_REGION_REDIRECT') 

1847 

1848 # Adding the new region to _cache will make construct_endpoint() to 

1849 # use the new region as value for the AWS::Region builtin parameter. 

1850 self._cache[bucket] = new_region 

1851 

1852 # Re-resolve endpoint with new region and modify request_dict with 

1853 # the new URL, auth scheme, and signing context. 

1854 ep_resolver = self._client._ruleset_resolver 

1855 ep_info = ep_resolver.construct_endpoint( 

1856 operation_model=operation, 

1857 call_args=request_dict['context']['s3_redirect']['params'], 

1858 request_context=request_dict['context'], 

1859 ) 

1860 request_dict['url'] = self.set_request_url( 

1861 request_dict['url'], ep_info.url 

1862 ) 

1863 request_dict['context']['s3_redirect']['redirected'] = True 

1864 auth_schemes = ep_info.properties.get('authSchemes') 

1865 if auth_schemes is not None: 

1866 auth_info = ep_resolver.auth_schemes_to_signing_ctx(auth_schemes) 

1867 auth_type, signing_context = auth_info 

1868 request_dict['context']['auth_type'] = auth_type 

1869 request_dict['context']['signing'] = { 

1870 **request_dict['context'].get('signing', {}), 

1871 **signing_context, 

1872 } 

1873 

1874 # Return 0 so it doesn't wait to retry 

1875 return 0 

1876 

1877 def get_bucket_region(self, bucket, response): 

1878 """ 

1879 There are multiple potential sources for the new region to redirect to, 

1880 but they aren't all universally available for use. This will try to 

1881 find region from response elements, but will fall back to calling 

1882 HEAD on the bucket if all else fails. 

1883 

1884 :param bucket: The bucket to find the region for. This is necessary if 

1885 the region is not available in the error response. 

1886 :param response: A response representing a service request that failed 

1887 due to incorrect region configuration. 

1888 """ 

1889 # First try to source the region from the headers. 

1890 service_response = response[1] 

1891 response_headers = service_response['ResponseMetadata']['HTTPHeaders'] 

1892 if 'x-amz-bucket-region' in response_headers: 

1893 region = response_headers['x-amz-bucket-region'] 

1894 # Next, check the error body 

1895 elif r := service_response.get('Error', {}).get('Region', None): 

1896 region = r 

1897 else: 

1898 # Finally, HEAD the bucket. No other choice sadly. 

1899 try: 

1900 response = self._client.head_bucket(Bucket=bucket) 

1901 headers = response['ResponseMetadata']['HTTPHeaders'] 

1902 except ClientError as e: 

1903 headers = e.response['ResponseMetadata']['HTTPHeaders'] 

1904 region = headers.get('x-amz-bucket-region', None) 

1905 validate_region_name(region) 

1906 return region 

1907 

1908 def set_request_url(self, old_url, new_endpoint, **kwargs): 

1909 """ 

1910 Splice a new endpoint into an existing URL. Note that some endpoints 

1911 from the the endpoint provider have a path component which will be 

1912 discarded by this function. 

1913 """ 

1914 return _get_new_endpoint(old_url, new_endpoint, False) 

1915 

1916 def redirect_from_cache(self, builtins, params, **kwargs): 

1917 """ 

1918 If a bucket name has been redirected before, it is in the cache. This 

1919 handler will update the AWS::Region endpoint resolver builtin param 

1920 to use the region from cache instead of the client region to avoid the 

1921 redirect. 

1922 """ 

1923 bucket = params.get('Bucket') 

1924 if bucket is not None and bucket in self._cache: 

1925 new_region = self._cache.get(bucket) 

1926 builtins['AWS::Region'] = new_region 

1927 register_feature_id('S3_REGION_REDIRECT') 

1928 

1929 def annotate_request_context(self, params, context, **kwargs): 

1930 """Store the bucket name in context for later use when redirecting. 

1931 The bucket name may be an access point ARN or alias. 

1932 """ 

1933 bucket = params.get('Bucket') 

1934 context['s3_redirect'] = { 

1935 'redirected': False, 

1936 'bucket': bucket, 

1937 'params': params, 

1938 } 

1939 

1940 

1941class S3RegionRedirector: 

1942 """This handler has been replaced by S3RegionRedirectorv2. The original 

1943 version remains in place for any third-party libraries that import it. 

1944 """ 

1945 

1946 def __init__(self, endpoint_bridge, client, cache=None): 

1947 self._endpoint_resolver = endpoint_bridge 

1948 self._cache = cache 

1949 if self._cache is None: 

1950 self._cache = {} 

1951 

1952 # This needs to be a weak ref in order to prevent memory leaks on 

1953 # python 2.6 

1954 self._client = weakref.proxy(client) 

1955 

1956 warnings.warn( 

1957 'The S3RegionRedirector class has been deprecated for a new ' 

1958 'internal replacement. A future version of botocore may remove ' 

1959 'this class.', 

1960 category=FutureWarning, 

1961 ) 

1962 

1963 def register(self, event_emitter=None): 

1964 emitter = event_emitter or self._client.meta.events 

1965 emitter.register('needs-retry.s3', self.redirect_from_error) 

1966 emitter.register('before-call.s3', self.set_request_url) 

1967 emitter.register('before-parameter-build.s3', self.redirect_from_cache) 

1968 

1969 def redirect_from_error(self, request_dict, response, operation, **kwargs): 

1970 """ 

1971 An S3 request sent to the wrong region will return an error that 

1972 contains the endpoint the request should be sent to. This handler 

1973 will add the redirect information to the signing context and then 

1974 redirect the request. 

1975 """ 

1976 if response is None: 

1977 # This could be none if there was a ConnectionError or other 

1978 # transport error. 

1979 return 

1980 

1981 if self._is_s3_accesspoint(request_dict.get('context', {})): 

1982 logger.debug( 

1983 'S3 request was previously to an accesspoint, not redirecting.' 

1984 ) 

1985 return 

1986 

1987 if request_dict.get('context', {}).get('s3_redirected'): 

1988 logger.debug( 

1989 'S3 request was previously redirected, not redirecting.' 

1990 ) 

1991 return 

1992 

1993 error = response[1].get('Error', {}) 

1994 error_code = error.get('Code') 

1995 response_metadata = response[1].get('ResponseMetadata', {}) 

1996 

1997 # We have to account for 400 responses because 

1998 # if we sign a Head* request with the wrong region, 

1999 # we'll get a 400 Bad Request but we won't get a 

2000 # body saying it's an "AuthorizationHeaderMalformed". 

2001 is_special_head_object = ( 

2002 error_code in ('301', '400') and operation.name == 'HeadObject' 

2003 ) 

2004 is_special_head_bucket = ( 

2005 error_code in ('301', '400') 

2006 and operation.name == 'HeadBucket' 

2007 and 'x-amz-bucket-region' 

2008 in response_metadata.get('HTTPHeaders', {}) 

2009 ) 

2010 is_wrong_signing_region = ( 

2011 error_code == 'AuthorizationHeaderMalformed' and 'Region' in error 

2012 ) 

2013 is_redirect_status = response[0] is not None and response[ 

2014 0 

2015 ].status_code in (301, 302, 307) 

2016 is_permanent_redirect = error_code == 'PermanentRedirect' 

2017 if not any( 

2018 [ 

2019 is_special_head_object, 

2020 is_wrong_signing_region, 

2021 is_permanent_redirect, 

2022 is_special_head_bucket, 

2023 is_redirect_status, 

2024 ] 

2025 ): 

2026 return 

2027 

2028 bucket = request_dict['context']['signing']['bucket'] 

2029 client_region = request_dict['context'].get('client_region') 

2030 new_region = self.get_bucket_region(bucket, response) 

2031 

2032 if new_region is None: 

2033 logger.debug( 

2034 "S3 client configured for region %s but the bucket %s is not " 

2035 "in that region and the proper region could not be " 

2036 "automatically determined.", 

2037 client_region, 

2038 bucket, 

2039 ) 

2040 return 

2041 

2042 logger.debug( 

2043 "S3 client configured for region %s but the bucket %s is in region" 

2044 " %s; Please configure the proper region to avoid multiple " 

2045 "unnecessary redirects and signing attempts.", 

2046 client_region, 

2047 bucket, 

2048 new_region, 

2049 ) 

2050 endpoint = self._endpoint_resolver.resolve('s3', new_region) 

2051 endpoint = endpoint['endpoint_url'] 

2052 

2053 signing_context = { 

2054 'region': new_region, 

2055 'bucket': bucket, 

2056 'endpoint': endpoint, 

2057 } 

2058 request_dict['context']['signing'] = signing_context 

2059 

2060 self._cache[bucket] = signing_context 

2061 self.set_request_url(request_dict, request_dict['context']) 

2062 

2063 request_dict['context']['s3_redirected'] = True 

2064 

2065 # Return 0 so it doesn't wait to retry 

2066 return 0 

2067 

2068 def get_bucket_region(self, bucket, response): 

2069 """ 

2070 There are multiple potential sources for the new region to redirect to, 

2071 but they aren't all universally available for use. This will try to 

2072 find region from response elements, but will fall back to calling 

2073 HEAD on the bucket if all else fails. 

2074 

2075 :param bucket: The bucket to find the region for. This is necessary if 

2076 the region is not available in the error response. 

2077 :param response: A response representing a service request that failed 

2078 due to incorrect region configuration. 

2079 """ 

2080 # First try to source the region from the headers. 

2081 service_response = response[1] 

2082 response_headers = service_response['ResponseMetadata']['HTTPHeaders'] 

2083 if 'x-amz-bucket-region' in response_headers: 

2084 return response_headers['x-amz-bucket-region'] 

2085 

2086 # Next, check the error body 

2087 region = service_response.get('Error', {}).get('Region', None) 

2088 if region is not None: 

2089 return region 

2090 

2091 # Finally, HEAD the bucket. No other choice sadly. 

2092 try: 

2093 response = self._client.head_bucket(Bucket=bucket) 

2094 headers = response['ResponseMetadata']['HTTPHeaders'] 

2095 except ClientError as e: 

2096 headers = e.response['ResponseMetadata']['HTTPHeaders'] 

2097 

2098 region = headers.get('x-amz-bucket-region', None) 

2099 return region 

2100 

2101 def set_request_url(self, params, context, **kwargs): 

2102 endpoint = context.get('signing', {}).get('endpoint', None) 

2103 if endpoint is not None: 

2104 params['url'] = _get_new_endpoint(params['url'], endpoint, False) 

2105 

2106 def redirect_from_cache(self, params, context, **kwargs): 

2107 """ 

2108 This handler retrieves a given bucket's signing context from the cache 

2109 and adds it into the request context. 

2110 """ 

2111 if self._is_s3_accesspoint(context): 

2112 return 

2113 bucket = params.get('Bucket') 

2114 signing_context = self._cache.get(bucket) 

2115 if signing_context is not None: 

2116 context['signing'] = signing_context 

2117 else: 

2118 context['signing'] = {'bucket': bucket} 

2119 

2120 def _is_s3_accesspoint(self, context): 

2121 return 's3_accesspoint' in context 

2122 

2123 

2124class InvalidArnException(ValueError): 

2125 pass 

2126 

2127 

2128class ArnParser: 

2129 def parse_arn(self, arn): 

2130 arn_parts = arn.split(':', 5) 

2131 if len(arn_parts) < 6: 

2132 raise InvalidArnException( 

2133 f'Provided ARN: {arn} must be of the format: ' 

2134 'arn:partition:service:region:account:resource' 

2135 ) 

2136 return { 

2137 'partition': arn_parts[1], 

2138 'service': arn_parts[2], 

2139 'region': arn_parts[3], 

2140 'account': arn_parts[4], 

2141 'resource': arn_parts[5], 

2142 } 

2143 

2144 @staticmethod 

2145 def is_arn(value): 

2146 if not isinstance(value, str) or not value.startswith('arn:'): 

2147 return False 

2148 arn_parser = ArnParser() 

2149 try: 

2150 arn_parser.parse_arn(value) 

2151 return True 

2152 except InvalidArnException: 

2153 return False 

2154 

2155 

2156class S3ArnParamHandler: 

2157 _RESOURCE_REGEX = re.compile( 

2158 r'^(?P<resource_type>accesspoint|outpost)[/:](?P<resource_name>.+)$' 

2159 ) 

2160 _OUTPOST_RESOURCE_REGEX = re.compile( 

2161 r'^(?P<outpost_name>[a-zA-Z0-9\-]{1,63})[/:]accesspoint[/:]' 

2162 r'(?P<accesspoint_name>[a-zA-Z0-9\-]{1,63}$)' 

2163 ) 

2164 _BLACKLISTED_OPERATIONS = ['CreateBucket'] 

2165 

2166 def __init__(self, arn_parser=None): 

2167 self._arn_parser = arn_parser 

2168 if arn_parser is None: 

2169 self._arn_parser = ArnParser() 

2170 

2171 def register(self, event_emitter): 

2172 event_emitter.register('before-parameter-build.s3', self.handle_arn) 

2173 

2174 def handle_arn(self, params, model, context, **kwargs): 

2175 if model.name in self._BLACKLISTED_OPERATIONS: 

2176 return 

2177 arn_details = self._get_arn_details_from_bucket_param(params) 

2178 if arn_details is None: 

2179 return 

2180 if arn_details['resource_type'] == 'accesspoint': 

2181 self._store_accesspoint(params, context, arn_details) 

2182 elif arn_details['resource_type'] == 'outpost': 

2183 self._store_outpost(params, context, arn_details) 

2184 

2185 def _get_arn_details_from_bucket_param(self, params): 

2186 if 'Bucket' in params: 

2187 try: 

2188 arn = params['Bucket'] 

2189 arn_details = self._arn_parser.parse_arn(arn) 

2190 self._add_resource_type_and_name(arn, arn_details) 

2191 return arn_details 

2192 except InvalidArnException: 

2193 pass 

2194 return None 

2195 

2196 def _add_resource_type_and_name(self, arn, arn_details): 

2197 match = self._RESOURCE_REGEX.match(arn_details['resource']) 

2198 if match: 

2199 arn_details['resource_type'] = match.group('resource_type') 

2200 arn_details['resource_name'] = match.group('resource_name') 

2201 else: 

2202 raise UnsupportedS3ArnError(arn=arn) 

2203 

2204 def _store_accesspoint(self, params, context, arn_details): 

2205 # Ideally the access-point would be stored as a parameter in the 

2206 # request where the serializer would then know how to serialize it, 

2207 # but access-points are not modeled in S3 operations so it would fail 

2208 # validation. Instead, we set the access-point to the bucket parameter 

2209 # to have some value set when serializing the request and additional 

2210 # information on the context from the arn to use in forming the 

2211 # access-point endpoint. 

2212 params['Bucket'] = arn_details['resource_name'] 

2213 context['s3_accesspoint'] = { 

2214 'name': arn_details['resource_name'], 

2215 'account': arn_details['account'], 

2216 'partition': arn_details['partition'], 

2217 'region': arn_details['region'], 

2218 'service': arn_details['service'], 

2219 } 

2220 

2221 def _store_outpost(self, params, context, arn_details): 

2222 resource_name = arn_details['resource_name'] 

2223 match = self._OUTPOST_RESOURCE_REGEX.match(resource_name) 

2224 if not match: 

2225 raise UnsupportedOutpostResourceError(resource_name=resource_name) 

2226 # Because we need to set the bucket name to something to pass 

2227 # validation we're going to use the access point name to be consistent 

2228 # with normal access point arns. 

2229 accesspoint_name = match.group('accesspoint_name') 

2230 params['Bucket'] = accesspoint_name 

2231 context['s3_accesspoint'] = { 

2232 'outpost_name': match.group('outpost_name'), 

2233 'name': accesspoint_name, 

2234 'account': arn_details['account'], 

2235 'partition': arn_details['partition'], 

2236 'region': arn_details['region'], 

2237 'service': arn_details['service'], 

2238 } 

2239 

2240 

2241class S3EndpointSetter: 

2242 _DEFAULT_PARTITION = 'aws' 

2243 _DEFAULT_DNS_SUFFIX = 'amazonaws.com' 

2244 

2245 def __init__( 

2246 self, 

2247 endpoint_resolver, 

2248 region=None, 

2249 s3_config=None, 

2250 endpoint_url=None, 

2251 partition=None, 

2252 use_fips_endpoint=False, 

2253 ): 

2254 # This is calling the endpoint_resolver in regions.py 

2255 self._endpoint_resolver = endpoint_resolver 

2256 self._region = region 

2257 self._s3_config = s3_config 

2258 self._use_fips_endpoint = use_fips_endpoint 

2259 if s3_config is None: 

2260 self._s3_config = {} 

2261 self._endpoint_url = endpoint_url 

2262 self._partition = partition 

2263 if partition is None: 

2264 self._partition = self._DEFAULT_PARTITION 

2265 

2266 def register(self, event_emitter): 

2267 event_emitter.register('before-sign.s3', self.set_endpoint) 

2268 event_emitter.register('choose-signer.s3', self.set_signer) 

2269 event_emitter.register( 

2270 'before-call.s3.WriteGetObjectResponse', 

2271 self.update_endpoint_to_s3_object_lambda, 

2272 ) 

2273 

2274 def update_endpoint_to_s3_object_lambda(self, params, context, **kwargs): 

2275 if self._use_accelerate_endpoint: 

2276 raise UnsupportedS3ConfigurationError( 

2277 msg='S3 client does not support accelerate endpoints for S3 Object Lambda operations', 

2278 ) 

2279 

2280 self._override_signing_name(context, 's3-object-lambda') 

2281 if self._endpoint_url: 

2282 # Only update the url if an explicit url was not provided 

2283 return 

2284 

2285 resolver = self._endpoint_resolver 

2286 # Constructing endpoints as s3-object-lambda as region 

2287 resolved = resolver.construct_endpoint( 

2288 's3-object-lambda', self._region 

2289 ) 

2290 

2291 # Ideally we would be able to replace the endpoint before 

2292 # serialization but there's no event to do that currently 

2293 # host_prefix is all the arn/bucket specs 

2294 new_endpoint = 'https://{host_prefix}{hostname}'.format( 

2295 host_prefix=params['host_prefix'], 

2296 hostname=resolved['hostname'], 

2297 ) 

2298 

2299 params['url'] = _get_new_endpoint(params['url'], new_endpoint, False) 

2300 

2301 def set_endpoint(self, request, **kwargs): 

2302 if self._use_accesspoint_endpoint(request): 

2303 self._validate_accesspoint_supported(request) 

2304 self._validate_fips_supported(request) 

2305 self._validate_global_regions(request) 

2306 region_name = self._resolve_region_for_accesspoint_endpoint( 

2307 request 

2308 ) 

2309 self._resolve_signing_name_for_accesspoint_endpoint(request) 

2310 self._switch_to_accesspoint_endpoint(request, region_name) 

2311 return 

2312 if self._use_accelerate_endpoint: 

2313 if self._use_fips_endpoint: 

2314 raise UnsupportedS3ConfigurationError( 

2315 msg=( 

2316 'Client is configured to use the FIPS psuedo region ' 

2317 f'for "{self._region}", but S3 Accelerate does not have any FIPS ' 

2318 'compatible endpoints.' 

2319 ) 

2320 ) 

2321 switch_host_s3_accelerate(request=request, **kwargs) 

2322 if self._s3_addressing_handler: 

2323 self._s3_addressing_handler(request=request, **kwargs) 

2324 

2325 def _use_accesspoint_endpoint(self, request): 

2326 return 's3_accesspoint' in request.context 

2327 

2328 def _validate_fips_supported(self, request): 

2329 if not self._use_fips_endpoint: 

2330 return 

2331 if 'fips' in request.context['s3_accesspoint']['region']: 

2332 raise UnsupportedS3AccesspointConfigurationError( 

2333 msg={'Invalid ARN, FIPS region not allowed in ARN.'} 

2334 ) 

2335 if 'outpost_name' in request.context['s3_accesspoint']: 

2336 raise UnsupportedS3AccesspointConfigurationError( 

2337 msg=( 

2338 f'Client is configured to use the FIPS psuedo-region "{self._region}", ' 

2339 'but outpost ARNs do not support FIPS endpoints.' 

2340 ) 

2341 ) 

2342 # Transforming psuedo region to actual region 

2343 accesspoint_region = request.context['s3_accesspoint']['region'] 

2344 if accesspoint_region != self._region: 

2345 if not self._s3_config.get('use_arn_region', True): 

2346 # TODO: Update message to reflect use_arn_region 

2347 # is not set 

2348 raise UnsupportedS3AccesspointConfigurationError( 

2349 msg=( 

2350 'Client is configured to use the FIPS psuedo-region ' 

2351 f'for "{self._region}", but the access-point ARN provided is for ' 

2352 f'the "{accesspoint_region}" region. For clients using a FIPS ' 

2353 'psuedo-region calls to access-point ARNs in another ' 

2354 'region are not allowed.' 

2355 ) 

2356 ) 

2357 

2358 def _validate_global_regions(self, request): 

2359 if self._s3_config.get('use_arn_region', True): 

2360 return 

2361 if self._region in ['aws-global', 's3-external-1']: 

2362 raise UnsupportedS3AccesspointConfigurationError( 

2363 msg=( 

2364 'Client is configured to use the global psuedo-region ' 

2365 f'"{self._region}". When providing access-point ARNs a regional ' 

2366 'endpoint must be specified.' 

2367 ) 

2368 ) 

2369 

2370 def _validate_accesspoint_supported(self, request): 

2371 if self._use_accelerate_endpoint: 

2372 raise UnsupportedS3AccesspointConfigurationError( 

2373 msg=( 

2374 'Client does not support s3 accelerate configuration ' 

2375 'when an access-point ARN is specified.' 

2376 ) 

2377 ) 

2378 request_partition = request.context['s3_accesspoint']['partition'] 

2379 if request_partition != self._partition: 

2380 raise UnsupportedS3AccesspointConfigurationError( 

2381 msg=( 

2382 f'Client is configured for "{self._partition}" partition, but access-point' 

2383 f' ARN provided is for "{request_partition}" partition. The client and ' 

2384 ' access-point partition must be the same.' 

2385 ) 

2386 ) 

2387 s3_service = request.context['s3_accesspoint'].get('service') 

2388 if s3_service == 's3-object-lambda' and self._s3_config.get( 

2389 'use_dualstack_endpoint' 

2390 ): 

2391 raise UnsupportedS3AccesspointConfigurationError( 

2392 msg=( 

2393 'Client does not support s3 dualstack configuration ' 

2394 'when an S3 Object Lambda access point ARN is specified.' 

2395 ) 

2396 ) 

2397 outpost_name = request.context['s3_accesspoint'].get('outpost_name') 

2398 if outpost_name and self._s3_config.get('use_dualstack_endpoint'): 

2399 raise UnsupportedS3AccesspointConfigurationError( 

2400 msg=( 

2401 'Client does not support s3 dualstack configuration ' 

2402 'when an outpost ARN is specified.' 

2403 ) 

2404 ) 

2405 self._validate_mrap_s3_config(request) 

2406 

2407 def _validate_mrap_s3_config(self, request): 

2408 if not is_global_accesspoint(request.context): 

2409 return 

2410 if self._s3_config.get('s3_disable_multiregion_access_points'): 

2411 raise UnsupportedS3AccesspointConfigurationError( 

2412 msg=( 

2413 'Invalid configuration, Multi-Region Access Point ' 

2414 'ARNs are disabled.' 

2415 ) 

2416 ) 

2417 elif self._s3_config.get('use_dualstack_endpoint'): 

2418 raise UnsupportedS3AccesspointConfigurationError( 

2419 msg=( 

2420 'Client does not support s3 dualstack configuration ' 

2421 'when a Multi-Region Access Point ARN is specified.' 

2422 ) 

2423 ) 

2424 

2425 def _resolve_region_for_accesspoint_endpoint(self, request): 

2426 if is_global_accesspoint(request.context): 

2427 # Requests going to MRAP endpoints MUST be set to any (*) region. 

2428 self._override_signing_region(request, '*') 

2429 elif self._s3_config.get('use_arn_region', True): 

2430 accesspoint_region = request.context['s3_accesspoint']['region'] 

2431 # If we are using the region from the access point, 

2432 # we will also want to make sure that we set it as the 

2433 # signing region as well 

2434 self._override_signing_region(request, accesspoint_region) 

2435 return accesspoint_region 

2436 return self._region 

2437 

2438 def set_signer(self, context, **kwargs): 

2439 if is_global_accesspoint(context): 

2440 if HAS_CRT: 

2441 return 's3v4a' 

2442 else: 

2443 raise MissingDependencyException( 

2444 msg="Using S3 with an MRAP arn requires an additional " 

2445 "dependency. You will need to pip install " 

2446 "botocore[crt] before proceeding." 

2447 ) 

2448 

2449 def _resolve_signing_name_for_accesspoint_endpoint(self, request): 

2450 accesspoint_service = request.context['s3_accesspoint']['service'] 

2451 self._override_signing_name(request.context, accesspoint_service) 

2452 

2453 def _switch_to_accesspoint_endpoint(self, request, region_name): 

2454 original_components = urlsplit(request.url) 

2455 accesspoint_endpoint = urlunsplit( 

2456 ( 

2457 original_components.scheme, 

2458 self._get_netloc(request.context, region_name), 

2459 self._get_accesspoint_path( 

2460 original_components.path, request.context 

2461 ), 

2462 original_components.query, 

2463 '', 

2464 ) 

2465 ) 

2466 logger.debug( 

2467 'Updating URI from %s to %s', request.url, accesspoint_endpoint 

2468 ) 

2469 request.url = accesspoint_endpoint 

2470 

2471 def _get_netloc(self, request_context, region_name): 

2472 if is_global_accesspoint(request_context): 

2473 return self._get_mrap_netloc(request_context) 

2474 else: 

2475 return self._get_accesspoint_netloc(request_context, region_name) 

2476 

2477 def _get_mrap_netloc(self, request_context): 

2478 s3_accesspoint = request_context['s3_accesspoint'] 

2479 region_name = 's3-global' 

2480 mrap_netloc_components = [s3_accesspoint['name']] 

2481 if self._endpoint_url: 

2482 endpoint_url_netloc = urlsplit(self._endpoint_url).netloc 

2483 mrap_netloc_components.append(endpoint_url_netloc) 

2484 else: 

2485 partition = s3_accesspoint['partition'] 

2486 mrap_netloc_components.extend( 

2487 [ 

2488 'accesspoint', 

2489 region_name, 

2490 self._get_partition_dns_suffix(partition), 

2491 ] 

2492 ) 

2493 return '.'.join(mrap_netloc_components) 

2494 

2495 def _get_accesspoint_netloc(self, request_context, region_name): 

2496 s3_accesspoint = request_context['s3_accesspoint'] 

2497 accesspoint_netloc_components = [ 

2498 '{}-{}'.format(s3_accesspoint['name'], s3_accesspoint['account']), 

2499 ] 

2500 outpost_name = s3_accesspoint.get('outpost_name') 

2501 if self._endpoint_url: 

2502 if outpost_name: 

2503 accesspoint_netloc_components.append(outpost_name) 

2504 endpoint_url_netloc = urlsplit(self._endpoint_url).netloc 

2505 accesspoint_netloc_components.append(endpoint_url_netloc) 

2506 else: 

2507 if outpost_name: 

2508 outpost_host = [outpost_name, 's3-outposts'] 

2509 accesspoint_netloc_components.extend(outpost_host) 

2510 elif s3_accesspoint['service'] == 's3-object-lambda': 

2511 component = self._inject_fips_if_needed( 

2512 's3-object-lambda', request_context 

2513 ) 

2514 accesspoint_netloc_components.append(component) 

2515 else: 

2516 component = self._inject_fips_if_needed( 

2517 's3-accesspoint', request_context 

2518 ) 

2519 accesspoint_netloc_components.append(component) 

2520 if self._s3_config.get('use_dualstack_endpoint'): 

2521 accesspoint_netloc_components.append('dualstack') 

2522 accesspoint_netloc_components.extend( 

2523 [region_name, self._get_dns_suffix(region_name)] 

2524 ) 

2525 return '.'.join(accesspoint_netloc_components) 

2526 

2527 def _inject_fips_if_needed(self, component, request_context): 

2528 if self._use_fips_endpoint: 

2529 return f'{component}-fips' 

2530 return component 

2531 

2532 def _get_accesspoint_path(self, original_path, request_context): 

2533 # The Bucket parameter was substituted with the access-point name as 

2534 # some value was required in serializing the bucket name. Now that 

2535 # we are making the request directly to the access point, we will 

2536 # want to remove that access-point name from the path. 

2537 name = request_context['s3_accesspoint']['name'] 

2538 # All S3 operations require at least a / in their path. 

2539 return original_path.replace('/' + name, '', 1) or '/' 

2540 

2541 def _get_partition_dns_suffix(self, partition_name): 

2542 dns_suffix = self._endpoint_resolver.get_partition_dns_suffix( 

2543 partition_name 

2544 ) 

2545 if dns_suffix is None: 

2546 dns_suffix = self._DEFAULT_DNS_SUFFIX 

2547 return dns_suffix 

2548 

2549 def _get_dns_suffix(self, region_name): 

2550 resolved = self._endpoint_resolver.construct_endpoint( 

2551 's3', region_name 

2552 ) 

2553 dns_suffix = self._DEFAULT_DNS_SUFFIX 

2554 if resolved and 'dnsSuffix' in resolved: 

2555 dns_suffix = resolved['dnsSuffix'] 

2556 return dns_suffix 

2557 

2558 def _override_signing_region(self, request, region_name): 

2559 signing_context = request.context.get('signing', {}) 

2560 # S3SigV4Auth will use the context['signing']['region'] value to 

2561 # sign with if present. This is used by the Bucket redirector 

2562 # as well but we should be fine because the redirector is never 

2563 # used in combination with the accesspoint setting logic. 

2564 signing_context['region'] = region_name 

2565 request.context['signing'] = signing_context 

2566 

2567 def _override_signing_name(self, context, signing_name): 

2568 signing_context = context.get('signing', {}) 

2569 # S3SigV4Auth will use the context['signing']['signing_name'] value to 

2570 # sign with if present. This is used by the Bucket redirector 

2571 # as well but we should be fine because the redirector is never 

2572 # used in combination with the accesspoint setting logic. 

2573 signing_context['signing_name'] = signing_name 

2574 context['signing'] = signing_context 

2575 

2576 @CachedProperty 

2577 def _use_accelerate_endpoint(self): 

2578 # Enable accelerate if the configuration is set to to true or the 

2579 # endpoint being used matches one of the accelerate endpoints. 

2580 

2581 # Accelerate has been explicitly configured. 

2582 if self._s3_config.get('use_accelerate_endpoint'): 

2583 return True 

2584 

2585 # Accelerate mode is turned on automatically if an endpoint url is 

2586 # provided that matches the accelerate scheme. 

2587 if self._endpoint_url is None: 

2588 return False 

2589 

2590 # Accelerate is only valid for Amazon endpoints. 

2591 netloc = urlsplit(self._endpoint_url).netloc 

2592 if not netloc.endswith('amazonaws.com'): 

2593 return False 

2594 

2595 # The first part of the url should always be s3-accelerate. 

2596 parts = netloc.split('.') 

2597 if parts[0] != 's3-accelerate': 

2598 return False 

2599 

2600 # Url parts between 's3-accelerate' and 'amazonaws.com' which 

2601 # represent different url features. 

2602 feature_parts = parts[1:-2] 

2603 

2604 # There should be no duplicate url parts. 

2605 if len(feature_parts) != len(set(feature_parts)): 

2606 return False 

2607 

2608 # Remaining parts must all be in the whitelist. 

2609 return all(p in S3_ACCELERATE_WHITELIST for p in feature_parts) 

2610 

2611 @CachedProperty 

2612 def _addressing_style(self): 

2613 # Use virtual host style addressing if accelerate is enabled or if 

2614 # the given endpoint url is an accelerate endpoint. 

2615 if self._use_accelerate_endpoint: 

2616 return 'virtual' 

2617 

2618 # If a particular addressing style is configured, use it. 

2619 configured_addressing_style = self._s3_config.get('addressing_style') 

2620 if configured_addressing_style: 

2621 return configured_addressing_style 

2622 

2623 @CachedProperty 

2624 def _s3_addressing_handler(self): 

2625 # If virtual host style was configured, use it regardless of whether 

2626 # or not the bucket looks dns compatible. 

2627 if self._addressing_style == 'virtual': 

2628 logger.debug("Using S3 virtual host style addressing.") 

2629 return switch_to_virtual_host_style 

2630 

2631 # If path style is configured, no additional steps are needed. If 

2632 # endpoint_url was specified, don't default to virtual. We could 

2633 # potentially default provided endpoint urls to virtual hosted 

2634 # style, but for now it is avoided. 

2635 if self._addressing_style == 'path' or self._endpoint_url is not None: 

2636 logger.debug("Using S3 path style addressing.") 

2637 return None 

2638 

2639 logger.debug( 

2640 "Defaulting to S3 virtual host style addressing with " 

2641 "path style addressing fallback." 

2642 ) 

2643 

2644 # By default, try to use virtual style with path fallback. 

2645 return fix_s3_host 

2646 

2647 

2648class S3ControlEndpointSetter: 

2649 _DEFAULT_PARTITION = 'aws' 

2650 _DEFAULT_DNS_SUFFIX = 'amazonaws.com' 

2651 _HOST_LABEL_REGEX = re.compile(r'^[a-zA-Z0-9\-]{1,63}$') 

2652 

2653 def __init__( 

2654 self, 

2655 endpoint_resolver, 

2656 region=None, 

2657 s3_config=None, 

2658 endpoint_url=None, 

2659 partition=None, 

2660 use_fips_endpoint=False, 

2661 ): 

2662 self._endpoint_resolver = endpoint_resolver 

2663 self._region = region 

2664 self._s3_config = s3_config 

2665 self._use_fips_endpoint = use_fips_endpoint 

2666 if s3_config is None: 

2667 self._s3_config = {} 

2668 self._endpoint_url = endpoint_url 

2669 self._partition = partition 

2670 if partition is None: 

2671 self._partition = self._DEFAULT_PARTITION 

2672 

2673 def register(self, event_emitter): 

2674 event_emitter.register('before-sign.s3-control', self.set_endpoint) 

2675 

2676 def set_endpoint(self, request, **kwargs): 

2677 if self._use_endpoint_from_arn_details(request): 

2678 self._validate_endpoint_from_arn_details_supported(request) 

2679 region_name = self._resolve_region_from_arn_details(request) 

2680 self._resolve_signing_name_from_arn_details(request) 

2681 self._resolve_endpoint_from_arn_details(request, region_name) 

2682 self._add_headers_from_arn_details(request) 

2683 elif self._use_endpoint_from_outpost_id(request): 

2684 self._validate_outpost_redirection_valid(request) 

2685 self._override_signing_name(request, 's3-outposts') 

2686 new_netloc = self._construct_outpost_endpoint(self._region) 

2687 self._update_request_netloc(request, new_netloc) 

2688 

2689 def _use_endpoint_from_arn_details(self, request): 

2690 return 'arn_details' in request.context 

2691 

2692 def _use_endpoint_from_outpost_id(self, request): 

2693 return 'outpost_id' in request.context 

2694 

2695 def _validate_endpoint_from_arn_details_supported(self, request): 

2696 if 'fips' in request.context['arn_details']['region']: 

2697 raise UnsupportedS3ControlArnError( 

2698 arn=request.context['arn_details']['original'], 

2699 msg='Invalid ARN, FIPS region not allowed in ARN.', 

2700 ) 

2701 if not self._s3_config.get('use_arn_region', False): 

2702 arn_region = request.context['arn_details']['region'] 

2703 if arn_region != self._region: 

2704 error_msg = ( 

2705 'The use_arn_region configuration is disabled but ' 

2706 f'received arn for "{arn_region}" when the client is configured ' 

2707 f'to use "{self._region}"' 

2708 ) 

2709 raise UnsupportedS3ControlConfigurationError(msg=error_msg) 

2710 request_partion = request.context['arn_details']['partition'] 

2711 if request_partion != self._partition: 

2712 raise UnsupportedS3ControlConfigurationError( 

2713 msg=( 

2714 f'Client is configured for "{self._partition}" partition, but arn ' 

2715 f'provided is for "{request_partion}" partition. The client and ' 

2716 'arn partition must be the same.' 

2717 ) 

2718 ) 

2719 if self._s3_config.get('use_accelerate_endpoint'): 

2720 raise UnsupportedS3ControlConfigurationError( 

2721 msg='S3 control client does not support accelerate endpoints', 

2722 ) 

2723 if 'outpost_name' in request.context['arn_details']: 

2724 self._validate_outpost_redirection_valid(request) 

2725 

2726 def _validate_outpost_redirection_valid(self, request): 

2727 if self._s3_config.get('use_dualstack_endpoint'): 

2728 raise UnsupportedS3ControlConfigurationError( 

2729 msg=( 

2730 'Client does not support s3 dualstack configuration ' 

2731 'when an outpost is specified.' 

2732 ) 

2733 ) 

2734 

2735 def _resolve_region_from_arn_details(self, request): 

2736 if self._s3_config.get('use_arn_region', False): 

2737 arn_region = request.context['arn_details']['region'] 

2738 # If we are using the region from the expanded arn, we will also 

2739 # want to make sure that we set it as the signing region as well 

2740 self._override_signing_region(request, arn_region) 

2741 return arn_region 

2742 return self._region 

2743 

2744 def _resolve_signing_name_from_arn_details(self, request): 

2745 arn_service = request.context['arn_details']['service'] 

2746 self._override_signing_name(request, arn_service) 

2747 return arn_service 

2748 

2749 def _resolve_endpoint_from_arn_details(self, request, region_name): 

2750 new_netloc = self._resolve_netloc_from_arn_details( 

2751 request, region_name 

2752 ) 

2753 self._update_request_netloc(request, new_netloc) 

2754 

2755 def _update_request_netloc(self, request, new_netloc): 

2756 original_components = urlsplit(request.url) 

2757 arn_details_endpoint = urlunsplit( 

2758 ( 

2759 original_components.scheme, 

2760 new_netloc, 

2761 original_components.path, 

2762 original_components.query, 

2763 '', 

2764 ) 

2765 ) 

2766 logger.debug( 

2767 'Updating URI from %s to %s', request.url, arn_details_endpoint 

2768 ) 

2769 request.url = arn_details_endpoint 

2770 

2771 def _resolve_netloc_from_arn_details(self, request, region_name): 

2772 arn_details = request.context['arn_details'] 

2773 if 'outpost_name' in arn_details: 

2774 return self._construct_outpost_endpoint(region_name) 

2775 account = arn_details['account'] 

2776 return self._construct_s3_control_endpoint(region_name, account) 

2777 

2778 def _is_valid_host_label(self, label): 

2779 return self._HOST_LABEL_REGEX.match(label) 

2780 

2781 def _validate_host_labels(self, *labels): 

2782 for label in labels: 

2783 if not self._is_valid_host_label(label): 

2784 raise InvalidHostLabelError(label=label) 

2785 

2786 def _construct_s3_control_endpoint(self, region_name, account): 

2787 self._validate_host_labels(region_name, account) 

2788 if self._endpoint_url: 

2789 endpoint_url_netloc = urlsplit(self._endpoint_url).netloc 

2790 netloc = [account, endpoint_url_netloc] 

2791 else: 

2792 netloc = [ 

2793 account, 

2794 's3-control', 

2795 ] 

2796 self._add_dualstack(netloc) 

2797 dns_suffix = self._get_dns_suffix(region_name) 

2798 netloc.extend([region_name, dns_suffix]) 

2799 return self._construct_netloc(netloc) 

2800 

2801 def _construct_outpost_endpoint(self, region_name): 

2802 self._validate_host_labels(region_name) 

2803 if self._endpoint_url: 

2804 return urlsplit(self._endpoint_url).netloc 

2805 else: 

2806 netloc = [ 

2807 's3-outposts', 

2808 region_name, 

2809 self._get_dns_suffix(region_name), 

2810 ] 

2811 self._add_fips(netloc) 

2812 return self._construct_netloc(netloc) 

2813 

2814 def _construct_netloc(self, netloc): 

2815 return '.'.join(netloc) 

2816 

2817 def _add_fips(self, netloc): 

2818 if self._use_fips_endpoint: 

2819 netloc[0] = netloc[0] + '-fips' 

2820 

2821 def _add_dualstack(self, netloc): 

2822 if self._s3_config.get('use_dualstack_endpoint'): 

2823 netloc.append('dualstack') 

2824 

2825 def _get_dns_suffix(self, region_name): 

2826 resolved = self._endpoint_resolver.construct_endpoint( 

2827 's3', region_name 

2828 ) 

2829 dns_suffix = self._DEFAULT_DNS_SUFFIX 

2830 if resolved and 'dnsSuffix' in resolved: 

2831 dns_suffix = resolved['dnsSuffix'] 

2832 return dns_suffix 

2833 

2834 def _override_signing_region(self, request, region_name): 

2835 signing_context = request.context.get('signing', {}) 

2836 # S3SigV4Auth will use the context['signing']['region'] value to 

2837 # sign with if present. This is used by the Bucket redirector 

2838 # as well but we should be fine because the redirector is never 

2839 # used in combination with the accesspoint setting logic. 

2840 signing_context['region'] = region_name 

2841 request.context['signing'] = signing_context 

2842 

2843 def _override_signing_name(self, request, signing_name): 

2844 signing_context = request.context.get('signing', {}) 

2845 # S3SigV4Auth will use the context['signing']['signing_name'] value to 

2846 # sign with if present. This is used by the Bucket redirector 

2847 # as well but we should be fine because the redirector is never 

2848 # used in combination with the accesspoint setting logic. 

2849 signing_context['signing_name'] = signing_name 

2850 request.context['signing'] = signing_context 

2851 

2852 def _add_headers_from_arn_details(self, request): 

2853 arn_details = request.context['arn_details'] 

2854 outpost_name = arn_details.get('outpost_name') 

2855 if outpost_name: 

2856 self._add_outpost_id_header(request, outpost_name) 

2857 

2858 def _add_outpost_id_header(self, request, outpost_name): 

2859 request.headers['x-amz-outpost-id'] = outpost_name 

2860 

2861 

2862class S3ControlArnParamHandler: 

2863 """This handler has been replaced by S3ControlArnParamHandlerv2. The 

2864 original version remains in place for any third-party importers. 

2865 """ 

2866 

2867 _RESOURCE_SPLIT_REGEX = re.compile(r'[/:]') 

2868 

2869 def __init__(self, arn_parser=None): 

2870 self._arn_parser = arn_parser 

2871 if arn_parser is None: 

2872 self._arn_parser = ArnParser() 

2873 warnings.warn( 

2874 'The S3ControlArnParamHandler class has been deprecated for a new ' 

2875 'internal replacement. A future version of botocore may remove ' 

2876 'this class.', 

2877 category=FutureWarning, 

2878 ) 

2879 

2880 def register(self, event_emitter): 

2881 event_emitter.register( 

2882 'before-parameter-build.s3-control', 

2883 self.handle_arn, 

2884 ) 

2885 

2886 def handle_arn(self, params, model, context, **kwargs): 

2887 if model.name in ('CreateBucket', 'ListRegionalBuckets'): 

2888 # CreateBucket and ListRegionalBuckets are special cases that do 

2889 # not obey ARN based redirection but will redirect based off of the 

2890 # presence of the OutpostId parameter 

2891 self._handle_outpost_id_param(params, model, context) 

2892 else: 

2893 self._handle_name_param(params, model, context) 

2894 self._handle_bucket_param(params, model, context) 

2895 

2896 def _get_arn_details_from_param(self, params, param_name): 

2897 if param_name not in params: 

2898 return None 

2899 try: 

2900 arn = params[param_name] 

2901 arn_details = self._arn_parser.parse_arn(arn) 

2902 arn_details['original'] = arn 

2903 arn_details['resources'] = self._split_resource(arn_details) 

2904 return arn_details 

2905 except InvalidArnException: 

2906 return None 

2907 

2908 def _split_resource(self, arn_details): 

2909 return self._RESOURCE_SPLIT_REGEX.split(arn_details['resource']) 

2910 

2911 def _override_account_id_param(self, params, arn_details): 

2912 account_id = arn_details['account'] 

2913 if 'AccountId' in params and params['AccountId'] != account_id: 

2914 error_msg = ( 

2915 'Account ID in arn does not match the AccountId parameter ' 

2916 'provided: "{}"' 

2917 ).format(params['AccountId']) 

2918 raise UnsupportedS3ControlArnError( 

2919 arn=arn_details['original'], 

2920 msg=error_msg, 

2921 ) 

2922 params['AccountId'] = account_id 

2923 

2924 def _handle_outpost_id_param(self, params, model, context): 

2925 if 'OutpostId' not in params: 

2926 return 

2927 context['outpost_id'] = params['OutpostId'] 

2928 

2929 def _handle_name_param(self, params, model, context): 

2930 # CreateAccessPoint is a special case that does not expand Name 

2931 if model.name == 'CreateAccessPoint': 

2932 return 

2933 arn_details = self._get_arn_details_from_param(params, 'Name') 

2934 if arn_details is None: 

2935 return 

2936 if self._is_outpost_accesspoint(arn_details): 

2937 self._store_outpost_accesspoint(params, context, arn_details) 

2938 else: 

2939 error_msg = 'The Name parameter does not support the provided ARN' 

2940 raise UnsupportedS3ControlArnError( 

2941 arn=arn_details['original'], 

2942 msg=error_msg, 

2943 ) 

2944 

2945 def _is_outpost_accesspoint(self, arn_details): 

2946 if arn_details['service'] != 's3-outposts': 

2947 return False 

2948 resources = arn_details['resources'] 

2949 if len(resources) != 4: 

2950 return False 

2951 # Resource must be of the form outpost/op-123/accesspoint/name 

2952 return resources[0] == 'outpost' and resources[2] == 'accesspoint' 

2953 

2954 def _store_outpost_accesspoint(self, params, context, arn_details): 

2955 self._override_account_id_param(params, arn_details) 

2956 accesspoint_name = arn_details['resources'][3] 

2957 params['Name'] = accesspoint_name 

2958 arn_details['accesspoint_name'] = accesspoint_name 

2959 arn_details['outpost_name'] = arn_details['resources'][1] 

2960 context['arn_details'] = arn_details 

2961 

2962 def _handle_bucket_param(self, params, model, context): 

2963 arn_details = self._get_arn_details_from_param(params, 'Bucket') 

2964 if arn_details is None: 

2965 return 

2966 if self._is_outpost_bucket(arn_details): 

2967 self._store_outpost_bucket(params, context, arn_details) 

2968 else: 

2969 error_msg = ( 

2970 'The Bucket parameter does not support the provided ARN' 

2971 ) 

2972 raise UnsupportedS3ControlArnError( 

2973 arn=arn_details['original'], 

2974 msg=error_msg, 

2975 ) 

2976 

2977 def _is_outpost_bucket(self, arn_details): 

2978 if arn_details['service'] != 's3-outposts': 

2979 return False 

2980 resources = arn_details['resources'] 

2981 if len(resources) != 4: 

2982 return False 

2983 # Resource must be of the form outpost/op-123/bucket/name 

2984 return resources[0] == 'outpost' and resources[2] == 'bucket' 

2985 

2986 def _store_outpost_bucket(self, params, context, arn_details): 

2987 self._override_account_id_param(params, arn_details) 

2988 bucket_name = arn_details['resources'][3] 

2989 params['Bucket'] = bucket_name 

2990 arn_details['bucket_name'] = bucket_name 

2991 arn_details['outpost_name'] = arn_details['resources'][1] 

2992 context['arn_details'] = arn_details 

2993 

2994 

2995class S3ControlArnParamHandlerv2(S3ControlArnParamHandler): 

2996 """Updated version of S3ControlArnParamHandler for use when 

2997 EndpointRulesetResolver is in use for endpoint resolution. 

2998 

2999 This class is considered private and subject to abrupt breaking changes or 

3000 removal without prior announcement. Please do not use it directly. 

3001 """ 

3002 

3003 def __init__(self, arn_parser=None): 

3004 self._arn_parser = arn_parser 

3005 if arn_parser is None: 

3006 self._arn_parser = ArnParser() 

3007 

3008 def register(self, event_emitter): 

3009 event_emitter.register( 

3010 'before-endpoint-resolution.s3-control', 

3011 self.handle_arn, 

3012 ) 

3013 

3014 def _handle_name_param(self, params, model, context): 

3015 # CreateAccessPoint is a special case that does not expand Name 

3016 if model.name == 'CreateAccessPoint': 

3017 return 

3018 arn_details = self._get_arn_details_from_param(params, 'Name') 

3019 if arn_details is None: 

3020 return 

3021 self._raise_for_fips_pseudo_region(arn_details) 

3022 self._raise_for_accelerate_endpoint(context) 

3023 if self._is_outpost_accesspoint(arn_details): 

3024 self._store_outpost_accesspoint(params, context, arn_details) 

3025 else: 

3026 error_msg = 'The Name parameter does not support the provided ARN' 

3027 raise UnsupportedS3ControlArnError( 

3028 arn=arn_details['original'], 

3029 msg=error_msg, 

3030 ) 

3031 

3032 def _store_outpost_accesspoint(self, params, context, arn_details): 

3033 self._override_account_id_param(params, arn_details) 

3034 

3035 def _handle_bucket_param(self, params, model, context): 

3036 arn_details = self._get_arn_details_from_param(params, 'Bucket') 

3037 if arn_details is None: 

3038 return 

3039 self._raise_for_fips_pseudo_region(arn_details) 

3040 self._raise_for_accelerate_endpoint(context) 

3041 if self._is_outpost_bucket(arn_details): 

3042 self._store_outpost_bucket(params, context, arn_details) 

3043 else: 

3044 error_msg = ( 

3045 'The Bucket parameter does not support the provided ARN' 

3046 ) 

3047 raise UnsupportedS3ControlArnError( 

3048 arn=arn_details['original'], 

3049 msg=error_msg, 

3050 ) 

3051 

3052 def _store_outpost_bucket(self, params, context, arn_details): 

3053 self._override_account_id_param(params, arn_details) 

3054 

3055 def _raise_for_fips_pseudo_region(self, arn_details): 

3056 # FIPS pseudo region names cannot be used in ARNs 

3057 arn_region = arn_details['region'] 

3058 if arn_region.startswith('fips-') or arn_region.endswith('fips-'): 

3059 raise UnsupportedS3ControlArnError( 

3060 arn=arn_details['original'], 

3061 msg='Invalid ARN, FIPS region not allowed in ARN.', 

3062 ) 

3063 

3064 def _raise_for_accelerate_endpoint(self, context): 

3065 s3_config = context['client_config'].s3 or {} 

3066 if s3_config.get('use_accelerate_endpoint'): 

3067 raise UnsupportedS3ControlConfigurationError( 

3068 msg='S3 control client does not support accelerate endpoints', 

3069 ) 

3070 

3071 

3072class ContainerMetadataFetcher: 

3073 TIMEOUT_SECONDS = 2 

3074 RETRY_ATTEMPTS = 3 

3075 SLEEP_TIME = 1 

3076 IP_ADDRESS = '169.254.170.2' 

3077 _ALLOWED_HOSTS = [ 

3078 IP_ADDRESS, 

3079 '169.254.170.23', 

3080 'fd00:ec2::23', 

3081 'localhost', 

3082 ] 

3083 

3084 def __init__(self, session=None, sleep=time.sleep): 

3085 if session is None: 

3086 session = botocore.httpsession.URLLib3Session( 

3087 timeout=self.TIMEOUT_SECONDS 

3088 ) 

3089 self._session = session 

3090 self._sleep = sleep 

3091 

3092 def retrieve_full_uri(self, full_url, headers=None): 

3093 """Retrieve JSON metadata from container metadata. 

3094 

3095 :type full_url: str 

3096 :param full_url: The full URL of the metadata service. 

3097 This should include the scheme as well, e.g 

3098 "http://localhost:123/foo" 

3099 

3100 """ 

3101 self._validate_allowed_url(full_url) 

3102 return self._retrieve_credentials(full_url, headers) 

3103 

3104 def _validate_allowed_url(self, full_url): 

3105 parsed = botocore.compat.urlparse(full_url) 

3106 

3107 if parsed.scheme == 'https': 

3108 return 

3109 if self._is_loopback_address(parsed.hostname): 

3110 return 

3111 is_whitelisted_host = self._check_if_whitelisted_host(parsed.hostname) 

3112 if not is_whitelisted_host: 

3113 raise ValueError( 

3114 f"Unsupported host '{parsed.hostname}'. Can only retrieve metadata " 

3115 f"from a loopback address or one of these hosts: {', '.join(self._ALLOWED_HOSTS)}" 

3116 ) 

3117 

3118 def _is_loopback_address(self, hostname): 

3119 try: 

3120 ip = ip_address(hostname) 

3121 return ip.is_loopback 

3122 except ValueError: 

3123 return False 

3124 

3125 def _check_if_whitelisted_host(self, host): 

3126 if host in self._ALLOWED_HOSTS: 

3127 return True 

3128 return False 

3129 

3130 def retrieve_uri(self, relative_uri): 

3131 """Retrieve JSON metadata from container metadata. 

3132 

3133 :type relative_uri: str 

3134 :param relative_uri: A relative URI, e.g "/foo/bar?id=123" 

3135 

3136 :return: The parsed JSON response. 

3137 

3138 """ 

3139 full_url = self.full_url(relative_uri) 

3140 return self._retrieve_credentials(full_url) 

3141 

3142 def _retrieve_credentials(self, full_url, extra_headers=None): 

3143 headers = {'Accept': 'application/json'} 

3144 if extra_headers is not None: 

3145 headers.update(extra_headers) 

3146 attempts = 0 

3147 while True: 

3148 try: 

3149 return self._get_response( 

3150 full_url, headers, self.TIMEOUT_SECONDS 

3151 ) 

3152 except MetadataRetrievalError as e: 

3153 logger.debug( 

3154 "Received error when attempting to retrieve " 

3155 "container metadata: %s", 

3156 e, 

3157 exc_info=True, 

3158 ) 

3159 self._sleep(self.SLEEP_TIME) 

3160 attempts += 1 

3161 if attempts >= self.RETRY_ATTEMPTS: 

3162 raise 

3163 

3164 def _get_response(self, full_url, headers, timeout): 

3165 try: 

3166 AWSRequest = botocore.awsrequest.AWSRequest 

3167 request = AWSRequest(method='GET', url=full_url, headers=headers) 

3168 response = self._session.send(request.prepare()) 

3169 response_text = response.content.decode('utf-8') 

3170 if response.status_code != 200: 

3171 raise MetadataRetrievalError( 

3172 error_msg=( 

3173 f"Received non 200 response {response.status_code} " 

3174 f"from container metadata: {response_text}" 

3175 ) 

3176 ) 

3177 try: 

3178 return json.loads(response_text) 

3179 except ValueError: 

3180 error_msg = "Unable to parse JSON returned from container metadata services" 

3181 logger.debug('%s:%s', error_msg, response_text) 

3182 raise MetadataRetrievalError(error_msg=error_msg) 

3183 except RETRYABLE_HTTP_ERRORS as e: 

3184 error_msg = ( 

3185 "Received error when attempting to retrieve " 

3186 f"container metadata: {e}" 

3187 ) 

3188 raise MetadataRetrievalError(error_msg=error_msg) 

3189 

3190 def full_url(self, relative_uri): 

3191 return f'http://{self.IP_ADDRESS}{relative_uri}' 

3192 

3193 

3194def get_environ_proxies(url): 

3195 if should_bypass_proxies(url): 

3196 return {} 

3197 else: 

3198 return getproxies() 

3199 

3200 

3201def should_bypass_proxies(url): 

3202 """ 

3203 Returns whether we should bypass proxies or not. 

3204 """ 

3205 # NOTE: requests allowed for ip/cidr entries in no_proxy env that we don't 

3206 # support current as urllib only checks DNS suffix 

3207 # If the system proxy settings indicate that this URL should be bypassed, 

3208 # don't proxy. 

3209 # The proxy_bypass function is incredibly buggy on OS X in early versions 

3210 # of Python 2.6, so allow this call to fail. Only catch the specific 

3211 # exceptions we've seen, though: this call failing in other ways can reveal 

3212 # legitimate problems. 

3213 try: 

3214 if proxy_bypass(urlparse(url).netloc): 

3215 return True 

3216 except (TypeError, socket.gaierror): 

3217 pass 

3218 

3219 return False 

3220 

3221 

3222def determine_content_length(body): 

3223 # No body, content length of 0 

3224 if not body: 

3225 return 0 

3226 

3227 # Try asking the body for it's length 

3228 try: 

3229 return len(body) 

3230 except (AttributeError, TypeError): 

3231 pass 

3232 

3233 # Try getting the length from a seekable stream 

3234 if hasattr(body, 'seek') and hasattr(body, 'tell'): 

3235 try: 

3236 orig_pos = body.tell() 

3237 body.seek(0, 2) 

3238 end_file_pos = body.tell() 

3239 body.seek(orig_pos) 

3240 return end_file_pos - orig_pos 

3241 except io.UnsupportedOperation: 

3242 # in case when body is, for example, io.BufferedIOBase object 

3243 # it has "seek" method which throws "UnsupportedOperation" 

3244 # exception in such case we want to fall back to "chunked" 

3245 # encoding 

3246 pass 

3247 # Failed to determine the length 

3248 return None 

3249 

3250 

3251def get_encoding_from_headers(headers, default='ISO-8859-1'): 

3252 """Returns encodings from given HTTP Header Dict. 

3253 

3254 :param headers: dictionary to extract encoding from. 

3255 :param default: default encoding if the content-type is text 

3256 """ 

3257 

3258 content_type = headers.get('content-type') 

3259 

3260 if not content_type: 

3261 return None 

3262 

3263 message = email.message.Message() 

3264 message['content-type'] = content_type 

3265 charset = message.get_param("charset") 

3266 

3267 if charset is not None: 

3268 return charset 

3269 

3270 if 'text' in content_type: 

3271 return default 

3272 

3273 

3274def calculate_md5(body, **kwargs): 

3275 """This function has been deprecated, but is kept for backwards compatibility.""" 

3276 if isinstance(body, (bytes, bytearray)): 

3277 binary_md5 = _calculate_md5_from_bytes(body) 

3278 else: 

3279 binary_md5 = _calculate_md5_from_file(body) 

3280 return base64.b64encode(binary_md5).decode('ascii') 

3281 

3282 

3283def _calculate_md5_from_bytes(body_bytes): 

3284 """This function has been deprecated, but is kept for backwards compatibility.""" 

3285 md5 = get_md5(body_bytes, usedforsecurity=False) 

3286 return md5.digest() 

3287 

3288 

3289def _calculate_md5_from_file(fileobj): 

3290 """This function has been deprecated, but is kept for backwards compatibility.""" 

3291 start_position = fileobj.tell() 

3292 md5 = get_md5(usedforsecurity=False) 

3293 for chunk in iter(lambda: fileobj.read(1024 * 1024), b''): 

3294 md5.update(chunk) 

3295 fileobj.seek(start_position) 

3296 return md5.digest() 

3297 

3298 

3299def _is_s3express_request(params): 

3300 endpoint_properties = params.get('context', {}).get( 

3301 'endpoint_properties', {} 

3302 ) 

3303 return endpoint_properties.get('backend') == 'S3Express' 

3304 

3305 

3306def get_checksum_algorithm_headers(params): 

3307 """ 

3308 Returns the a list of header names from the request which start with 

3309 "x-amz-checksum-", otherwise returns an empty list. 

3310 

3311 This function is considered private and subject to abrupt breaking changes or 

3312 removal without prior announcement. Please do not use it directly. 

3313 """ 

3314 headers = params['headers'] 

3315 checksum_headers = [] 

3316 

3317 # If a header matching the x-amz-checksum-* pattern is present, we 

3318 # extract and return the algorithm name. 

3319 for header in headers: 

3320 match = CHECKSUM_HEADER_PATTERN.match(header) 

3321 if match: 

3322 checksum_headers.append(header) 

3323 return checksum_headers 

3324 

3325 

3326def has_checksum_header(params): 

3327 """ 

3328 Checks if a header starting with "x-amz-checksum-" is provided in a request. 

3329 

3330 This function is considered private and subject to abrupt breaking changes or 

3331 removal without prior announcement. Please do not use it directly. 

3332 """ 

3333 return bool(get_checksum_algorithm_headers(params)) 

3334 

3335 

3336def conditionally_calculate_checksum(params, **kwargs): 

3337 """This function has been deprecated, but is kept for backwards compatibility.""" 

3338 if not has_checksum_header(params): 

3339 conditionally_calculate_md5(params, **kwargs) 

3340 conditionally_enable_crc32(params, **kwargs) 

3341 

3342 

3343def conditionally_enable_crc32(params, **kwargs): 

3344 """This function has been deprecated, but is kept for backwards compatibility.""" 

3345 checksum_context = params.get('context', {}).get('checksum', {}) 

3346 checksum_algorithm = checksum_context.get('request_algorithm') 

3347 if ( 

3348 _is_s3express_request(params) 

3349 and params['body'] is not None 

3350 and checksum_algorithm in (None, "conditional-md5") 

3351 ): 

3352 params['context']['checksum'] = { 

3353 'request_algorithm': { 

3354 'algorithm': 'crc32', 

3355 'in': 'header', 

3356 'name': 'x-amz-checksum-crc32', 

3357 } 

3358 } 

3359 

3360 

3361def conditionally_calculate_md5(params, **kwargs): 

3362 """Only add a Content-MD5 if the system supports it. 

3363 

3364 This function has been deprecated, but is kept for backwards compatibility. 

3365 """ 

3366 body = params['body'] 

3367 checksum_context = params.get('context', {}).get('checksum', {}) 

3368 checksum_algorithm = checksum_context.get('request_algorithm') 

3369 if checksum_algorithm and checksum_algorithm != 'conditional-md5': 

3370 # Skip for requests that will have a flexible checksum applied 

3371 return 

3372 

3373 if has_checksum_header(params): 

3374 # Don't add a new header if one is already available. 

3375 return 

3376 

3377 if _is_s3express_request(params): 

3378 # S3Express doesn't support MD5 

3379 return 

3380 

3381 if MD5_AVAILABLE and body is not None: 

3382 md5_digest = calculate_md5(body, **kwargs) 

3383 params['headers']['Content-MD5'] = md5_digest 

3384 

3385 

3386class FileWebIdentityTokenLoader: 

3387 def __init__(self, web_identity_token_path, _open=open): 

3388 self._web_identity_token_path = web_identity_token_path 

3389 self._open = _open 

3390 

3391 def __call__(self): 

3392 with self._open(self._web_identity_token_path) as token_file: 

3393 return token_file.read() 

3394 

3395 

3396class SSOTokenLoader: 

3397 def __init__(self, cache=None): 

3398 if cache is None: 

3399 cache = {} 

3400 self._cache = cache 

3401 

3402 def _generate_cache_key(self, start_url, session_name): 

3403 input_str = start_url 

3404 if session_name is not None: 

3405 input_str = session_name 

3406 return hashlib.sha1(input_str.encode('utf-8')).hexdigest() 

3407 

3408 def save_token(self, start_url, token, session_name=None): 

3409 cache_key = self._generate_cache_key(start_url, session_name) 

3410 self._cache[cache_key] = token 

3411 

3412 def __call__(self, start_url, session_name=None): 

3413 cache_key = self._generate_cache_key(start_url, session_name) 

3414 logger.debug('Checking for cached token at: %s', cache_key) 

3415 if cache_key not in self._cache: 

3416 name = start_url 

3417 if session_name is not None: 

3418 name = session_name 

3419 error_msg = f'Token for {name} does not exist' 

3420 raise SSOTokenLoadError(error_msg=error_msg) 

3421 

3422 token = self._cache[cache_key] 

3423 if 'accessToken' not in token or 'expiresAt' not in token: 

3424 error_msg = f'Token for {start_url} is invalid' 

3425 raise SSOTokenLoadError(error_msg=error_msg) 

3426 return token 

3427 

3428 

3429class EventbridgeSignerSetter: 

3430 _DEFAULT_PARTITION = 'aws' 

3431 _DEFAULT_DNS_SUFFIX = 'amazonaws.com' 

3432 

3433 def __init__(self, endpoint_resolver, region=None, endpoint_url=None): 

3434 self._endpoint_resolver = endpoint_resolver 

3435 self._region = region 

3436 self._endpoint_url = endpoint_url 

3437 

3438 def register(self, event_emitter): 

3439 event_emitter.register( 

3440 'before-parameter-build.events.PutEvents', 

3441 self.check_for_global_endpoint, 

3442 ) 

3443 event_emitter.register( 

3444 'before-call.events.PutEvents', self.set_endpoint_url 

3445 ) 

3446 

3447 def set_endpoint_url(self, params, context, **kwargs): 

3448 if 'eventbridge_endpoint' in context: 

3449 endpoint = context['eventbridge_endpoint'] 

3450 logger.debug( 

3451 "Rewriting URL from %s to %s", params['url'], endpoint 

3452 ) 

3453 params['url'] = endpoint 

3454 

3455 def check_for_global_endpoint(self, params, context, **kwargs): 

3456 endpoint = params.get('EndpointId') 

3457 if endpoint is None: 

3458 return 

3459 

3460 if len(endpoint) == 0: 

3461 raise InvalidEndpointConfigurationError( 

3462 msg='EndpointId must not be a zero length string' 

3463 ) 

3464 

3465 if not HAS_CRT: 

3466 raise MissingDependencyException( 

3467 msg="Using EndpointId requires an additional " 

3468 "dependency. You will need to pip install " 

3469 "botocore[crt] before proceeding." 

3470 ) 

3471 

3472 config = context.get('client_config') 

3473 endpoint_variant_tags = None 

3474 if config is not None: 

3475 if config.use_fips_endpoint: 

3476 raise InvalidEndpointConfigurationError( 

3477 msg="FIPS is not supported with EventBridge " 

3478 "multi-region endpoints." 

3479 ) 

3480 if config.use_dualstack_endpoint: 

3481 endpoint_variant_tags = ['dualstack'] 

3482 

3483 if self._endpoint_url is None: 

3484 # Validate endpoint is a valid hostname component 

3485 parts = urlparse(f'https://{endpoint}') 

3486 if parts.hostname != endpoint: 

3487 raise InvalidEndpointConfigurationError( 

3488 msg='EndpointId is not a valid hostname component.' 

3489 ) 

3490 resolved_endpoint = self._get_global_endpoint( 

3491 endpoint, endpoint_variant_tags=endpoint_variant_tags 

3492 ) 

3493 else: 

3494 resolved_endpoint = self._endpoint_url 

3495 

3496 context['eventbridge_endpoint'] = resolved_endpoint 

3497 context['auth_type'] = 'v4a' 

3498 

3499 def _get_global_endpoint(self, endpoint, endpoint_variant_tags=None): 

3500 resolver = self._endpoint_resolver 

3501 

3502 partition = resolver.get_partition_for_region(self._region) 

3503 if partition is None: 

3504 partition = self._DEFAULT_PARTITION 

3505 dns_suffix = resolver.get_partition_dns_suffix( 

3506 partition, endpoint_variant_tags=endpoint_variant_tags 

3507 ) 

3508 if dns_suffix is None: 

3509 dns_suffix = self._DEFAULT_DNS_SUFFIX 

3510 

3511 return f"https://{endpoint}.endpoint.events.{dns_suffix}/" 

3512 

3513 

3514def is_s3_accelerate_url(url): 

3515 """Does the URL match the S3 Accelerate endpoint scheme? 

3516 

3517 Virtual host naming style with bucket names in the netloc part of the URL 

3518 are not allowed by this function. 

3519 """ 

3520 if url is None: 

3521 return False 

3522 

3523 # Accelerate is only valid for Amazon endpoints. 

3524 url_parts = urlsplit(url) 

3525 if not url_parts.netloc.endswith( 

3526 'amazonaws.com' 

3527 ) or url_parts.scheme not in ['https', 'http']: 

3528 return False 

3529 

3530 # The first part of the URL must be s3-accelerate. 

3531 parts = url_parts.netloc.split('.') 

3532 if parts[0] != 's3-accelerate': 

3533 return False 

3534 

3535 # Url parts between 's3-accelerate' and 'amazonaws.com' which 

3536 # represent different url features. 

3537 feature_parts = parts[1:-2] 

3538 

3539 # There should be no duplicate URL parts. 

3540 if len(feature_parts) != len(set(feature_parts)): 

3541 return False 

3542 

3543 # Remaining parts must all be in the whitelist. 

3544 return all(p in S3_ACCELERATE_WHITELIST for p in feature_parts) 

3545 

3546 

3547class JSONFileCache: 

3548 """JSON file cache. 

3549 This provides a dict like interface that stores JSON serializable 

3550 objects. 

3551 The objects are serialized to JSON and stored in a file. These 

3552 values can be retrieved at a later time. 

3553 """ 

3554 

3555 CACHE_DIR = os.path.expanduser(os.path.join('~', '.aws', 'boto', 'cache')) 

3556 

3557 def __init__(self, working_dir=CACHE_DIR, dumps_func=None): 

3558 self._working_dir = working_dir 

3559 if dumps_func is None: 

3560 dumps_func = self._default_dumps 

3561 self._dumps = dumps_func 

3562 

3563 def _default_dumps(self, obj): 

3564 return json.dumps(obj, default=self._serialize_if_needed) 

3565 

3566 def __contains__(self, cache_key): 

3567 actual_key = self._convert_cache_key(cache_key) 

3568 return os.path.isfile(actual_key) 

3569 

3570 def __getitem__(self, cache_key): 

3571 """Retrieve value from a cache key.""" 

3572 actual_key = self._convert_cache_key(cache_key) 

3573 try: 

3574 with open(actual_key) as f: 

3575 return json.load(f) 

3576 except (OSError, ValueError): 

3577 raise KeyError(cache_key) 

3578 

3579 def __delitem__(self, cache_key): 

3580 actual_key = self._convert_cache_key(cache_key) 

3581 try: 

3582 key_path = Path(actual_key) 

3583 key_path.unlink() 

3584 except FileNotFoundError: 

3585 raise KeyError(cache_key) 

3586 

3587 def __setitem__(self, cache_key, value): 

3588 full_key = self._convert_cache_key(cache_key) 

3589 try: 

3590 file_content = self._dumps(value) 

3591 except (TypeError, ValueError): 

3592 raise ValueError( 

3593 f"Value cannot be cached, must be JSON serializable: {value}" 

3594 ) 

3595 if not os.path.isdir(self._working_dir): 

3596 os.makedirs(self._working_dir, exist_ok=True) 

3597 

3598 temp_fd, temp_path = tempfile.mkstemp( 

3599 dir=self._working_dir, suffix='.tmp' 

3600 ) 

3601 with os.fdopen(temp_fd, 'w') as f: 

3602 f.write(file_content) 

3603 f.flush() 

3604 os.fsync(f.fileno()) 

3605 

3606 os.replace(temp_path, full_key) 

3607 

3608 def _convert_cache_key(self, cache_key): 

3609 full_path = os.path.join(self._working_dir, cache_key + '.json') 

3610 return full_path 

3611 

3612 def _serialize_if_needed(self, value, iso=False): 

3613 if isinstance(value, _DatetimeClass): 

3614 if iso: 

3615 return value.isoformat() 

3616 return value.strftime('%Y-%m-%dT%H:%M:%S%Z') 

3617 return value 

3618 

3619 

3620def generate_login_cache_key(sign_in_session_name): 

3621 return hashlib.sha256(sign_in_session_name.encode('utf-8')).hexdigest() 

3622 

3623 

3624def is_s3express_bucket(bucket): 

3625 if bucket is None: 

3626 return False 

3627 return bucket.endswith('--x-s3') 

3628 

3629 

3630def get_token_from_environment(signing_name, environ=None): 

3631 if not isinstance(signing_name, str) or not signing_name.strip(): 

3632 return None 

3633 

3634 if environ is None: 

3635 environ = os.environ 

3636 env_var = _get_bearer_env_var_name(signing_name) 

3637 return environ.get(env_var) 

3638 

3639 

3640def _get_bearer_env_var_name(signing_name): 

3641 bearer_name = signing_name.replace('-', '_').replace(' ', '_').upper() 

3642 return f"AWS_BEARER_TOKEN_{bearer_name}" 

3643 

3644 

3645# This parameter is not part of the public interface and is subject to abrupt 

3646# breaking changes or removal without prior announcement. 

3647# Mapping of services that have been renamed for backwards compatibility reasons. 

3648# Keys are the previous name that should be allowed, values are the documented 

3649# and preferred client name. 

3650SERVICE_NAME_ALIASES = {'runtime.sagemaker': 'sagemaker-runtime'} 

3651 

3652 

3653# This parameter is not part of the public interface and is subject to abrupt 

3654# breaking changes or removal without prior announcement. 

3655# Mapping to determine the service ID for services that do not use it as the 

3656# model data directory name. The keys are the data directory name and the 

3657# values are the transformed service IDs (lower case and hyphenated). 

3658CLIENT_NAME_TO_HYPHENIZED_SERVICE_ID_OVERRIDES = { 

3659 # Actual service name we use -> Allowed computed service name. 

3660 'apigateway': 'api-gateway', 

3661 'application-autoscaling': 'application-auto-scaling', 

3662 'appmesh': 'app-mesh', 

3663 'autoscaling': 'auto-scaling', 

3664 'autoscaling-plans': 'auto-scaling-plans', 

3665 'ce': 'cost-explorer', 

3666 'cloudhsmv2': 'cloudhsm-v2', 

3667 'cloudsearchdomain': 'cloudsearch-domain', 

3668 'cognito-idp': 'cognito-identity-provider', 

3669 'config': 'config-service', 

3670 'cur': 'cost-and-usage-report-service', 

3671 'datapipeline': 'data-pipeline', 

3672 'directconnect': 'direct-connect', 

3673 'devicefarm': 'device-farm', 

3674 'discovery': 'application-discovery-service', 

3675 'dms': 'database-migration-service', 

3676 'ds': 'directory-service', 

3677 'ds-data': 'directory-service-data', 

3678 'dynamodbstreams': 'dynamodb-streams', 

3679 'elasticbeanstalk': 'elastic-beanstalk', 

3680 'elb': 'elastic-load-balancing', 

3681 'elbv2': 'elastic-load-balancing-v2', 

3682 'es': 'elasticsearch-service', 

3683 'events': 'eventbridge', 

3684 'globalaccelerator': 'global-accelerator', 

3685 'iot-data': 'iot-data-plane', 

3686 'iot-jobs-data': 'iot-jobs-data-plane', 

3687 'iotwireless': 'iot-wireless', 

3688 'kinesisanalytics': 'kinesis-analytics', 

3689 'kinesisanalyticsv2': 'kinesis-analytics-v2', 

3690 'kinesisvideo': 'kinesis-video', 

3691 'lex-models': 'lex-model-building-service', 

3692 'lexv2-models': 'lex-models-v2', 

3693 'lex-runtime': 'lex-runtime-service', 

3694 'lexv2-runtime': 'lex-runtime-v2', 

3695 'logs': 'cloudwatch-logs', 

3696 'machinelearning': 'machine-learning', 

3697 'marketplacecommerceanalytics': 'marketplace-commerce-analytics', 

3698 'marketplace-entitlement': 'marketplace-entitlement-service', 

3699 'meteringmarketplace': 'marketplace-metering', 

3700 'mgh': 'migration-hub', 

3701 'sms-voice': 'pinpoint-sms-voice', 

3702 'resourcegroupstaggingapi': 'resource-groups-tagging-api', 

3703 'route53': 'route-53', 

3704 'route53domains': 'route-53-domains', 

3705 's3control': 's3-control', 

3706 'sdb': 'simpledb', 

3707 'secretsmanager': 'secrets-manager', 

3708 'serverlessrepo': 'serverlessapplicationrepository', 

3709 'servicecatalog': 'service-catalog', 

3710 'servicecatalog-appregistry': 'service-catalog-appregistry', 

3711 'stepfunctions': 'sfn', 

3712 'storagegateway': 'storage-gateway', 

3713} 

3714 

3715 

3716def get_login_token_cache_directory(): 

3717 """Returns which directory contains the login_session token files""" 

3718 if 'AWS_LOGIN_CACHE_DIRECTORY' in os.environ: 

3719 path = os.path.expandvars(os.environ['AWS_LOGIN_CACHE_DIRECTORY']) 

3720 path = os.path.expanduser(path) 

3721 return path 

3722 else: 

3723 return os.path.expanduser(os.path.join('~', '.aws', 'login', 'cache')) 

3724 

3725 

3726class LoginTokenLoader: 

3727 """Loads and saves login access tokens to disk""" 

3728 

3729 def __init__(self, cache=None): 

3730 if cache is None: 

3731 cache = {} 

3732 self._cache = cache 

3733 

3734 def save_token(self, session_name, token): 

3735 cache_key = generate_login_cache_key(session_name) 

3736 self._cache[cache_key] = token 

3737 

3738 def load_token(self, session_name): 

3739 cache_key = generate_login_cache_key(session_name) 

3740 if cache_key not in self._cache: 

3741 return None 

3742 return self._cache[cache_key]