python-botocore/botocore/handlers.py
2015-10-08 11:15:29 -07:00

208 lines
7.6 KiB
Python

# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish, dis-
# tribute, sublicense, and/or sell copies of the Software, and to permit
# persons to whom the Software is furnished to do so, subject to the fol-
# lowing conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#
"""Builtin event handlers.
This module contains builtin handlers for events emitted by botocore.
"""
import base64
import hashlib
import logging
import re
import six
from botocore.compat import urlsplit, urlunsplit, unquote, json
from botocore import retryhandler
import botocore.auth
logger = logging.getLogger(__name__)
LABEL_RE = re.compile('[a-z0-9][a-z0-9\-]*[a-z0-9]')
RESTRICTED_REGIONS = [
'us-gov-west-1',
'fips-gov-west-1',
]
def decode_console_output(event_name, shape, value, **kwargs):
try:
value = base64.b64decode(six.b(value)).decode('utf-8')
except TypeError:
logger.debug('Error decoding base64', exc_info=True)
return value
def decode_quoted_jsondoc(event_name, shape, value, **kwargs):
try:
value = json.loads(unquote(value))
except (ValueError, TypeError):
logger.debug('Error loading quoted JSON', exc_info=True)
return value
def decode_jsondoc(event_name, shape, value, **kwargs):
try:
value = json.loads(value)
except (ValueError, TypeError):
logger.debug('error loading JSON', exc_info=True)
return value
def calculate_md5(event_name, params, **kwargs):
if params['payload'] and not 'Content-MD5' in params['headers']:
md5 = hashlib.md5()
md5.update(six.b(params['payload'].getvalue()))
value = base64.b64encode(md5.digest()).decode('utf-8')
params['headers']['Content-MD5'] = value
def check_dns_name(bucket_name):
"""
Check to see if the ``bucket_name`` complies with the
restricted DNS naming conventions necessary to allow
access via virtual-hosting style.
Even though "." characters are perfectly valid in this DNS
naming scheme, we are going to punt on any name containing a
"." character because these will cause SSL cert validation
problems if we try to use virtual-hosting style addressing.
"""
if '.' in bucket_name:
return False
n = len(bucket_name)
if n < 3 or n > 63:
# Wrong length
return False
if n == 1:
if not bucket_name.isalnum():
return False
match = LABEL_RE.match(bucket_name)
if match is None or match.end() != len(bucket_name):
return False
return True
def fix_s3_host(event_name, endpoint, request, auth, **kwargs):
"""
This handler looks at S3 requests just before they are signed.
If there is a bucket name on the path (true for everything except
ListAllBuckets) it checks to see if that bucket name conforms to
the DNS naming conventions. If it does, it alters the request to
use ``virtual hosting`` style addressing rather than ``path-style``
addressing. This allows us to avoid 301 redirects for all
bucket names that can be CNAME'd.
"""
parts = urlsplit(request.url)
auth.auth_path = parts.path
path_parts = parts.path.split('/')
if isinstance(auth, botocore.auth.S3SigV4Auth):
return
if len(path_parts) > 1:
bucket_name = path_parts[1]
logger.debug('Checking for DNS compatible bucket for: %s',
request.url)
if check_dns_name(bucket_name) and _allowed_region(endpoint.region_name):
# If the operation is on a bucket, the auth_path must be
# terminated with a '/' character.
if len(path_parts) == 2:
if auth.auth_path[-1] != '/':
auth.auth_path += '/'
path_parts.remove(bucket_name)
host = bucket_name + '.' + endpoint.service.global_endpoint
new_tuple = (parts.scheme, host, '/'.join(path_parts),
parts.query, '')
new_uri = urlunsplit(new_tuple)
request.url = new_uri
logger.debug('URI updated to: %s', new_uri)
else:
logger.debug('Not changing URI, bucket is not DNS compatible: %s',
bucket_name)
def _allowed_region(region_name):
return region_name not in RESTRICTED_REGIONS
def register_retries_for_service(service, **kwargs):
if not hasattr(service, 'retry'):
return
logger.debug("Registering retry handlers for service: %s", service)
config = service.retry
session = service.session
handler = retryhandler.create_retry_handler(config)
unique_id = 'retry-config-%s' % service.endpoint_prefix
session.register('needs-retry.%s' % service.endpoint_prefix,
handler, unique_id=unique_id)
_register_for_operations(config, session,
service_name=service.endpoint_prefix)
def _register_for_operations(config, session, service_name):
# There's certainly a tradeoff for registering the retry config
# for the operations when the service is created. In practice,
# there aren't a whole lot of per operation retry configs so
# this is ok for now.
for key in config:
if key == '__default__':
continue
handler = retryhandler.create_retry_handler(config, key)
unique_id = 'retry-config-%s-%s' % (service_name, key)
session.register('needs-retry.%s.%s' % (service_name, key),
handler, unique_id=unique_id)
def maybe_switch_to_s3sigv4(service, region_name, **kwargs):
if region_name.startswith('cn-'):
# This region only supports signature version 4 for
# s3, so we need to change the service's signature version.
service.signature_version = 's3v4'
def maybe_switch_to_sigv4(service, region_name, **kwargs):
if region_name.startswith('cn-'):
# This region only supports signature version 4 for
# s3, so we need to change the service's signature version.
service.signature_version = 'v4'
# This is a list of (event_name, handler).
# When a Session is created, everything in this list will be
# automatically registered with that Session.
BUILTIN_HANDLERS = [
('after-parsed.ec2.GetConsoleOutput.String.Output',
decode_console_output),
('after-parsed.iam.*.policyDocumentType.*',
decode_quoted_jsondoc),
('after-parsed.cloudformation.*.TemplateBody.TemplateBody',
decode_jsondoc),
('before-call.s3.PutBucketTagging', calculate_md5),
('before-call.s3.PutBucketLifecycle', calculate_md5),
('before-call.s3.PutBucketCors', calculate_md5),
('before-call.s3.DeleteObjects', calculate_md5),
('before-auth.s3', fix_s3_host),
('service-created', register_retries_for_service),
('creating-endpoint.s3', maybe_switch_to_s3sigv4),
('creating-endpoint.ec2', maybe_switch_to_sigv4),
]