# Copyright 2012-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import logging
import os
import shutil
import tempfile
import threading
import time
from collections import defaultdict
from contextlib import closing
from io import BytesIO
from tarfile import TarFile

import pytest
import urllib3

import botocore.auth
import botocore.credentials
import botocore.session
from botocore.compat import OrderedDict, zip_longest
from botocore.config import Config
from botocore.exceptions import ClientError, ConnectionClosedError, WaiterError
from tests import (
    ClientHTTPStubber,
    ConsistencyWaiter,
    random_chars,
    temporary_file,
    unittest,
)


def random_bucketname():
    return 'botocoretest-' + random_chars(50)


LOG = logging.getLogger('botocore.tests.integration')
_SHARED_BUCKET = random_bucketname()
_DEFAULT_REGION = 'us-west-2'


def http_get(url):
    http = urllib3.PoolManager(cert_reqs='CERT_REQUIRED')
    response = http.request('GET', url)
    return response


def http_post(url, data, files):
    http = urllib3.PoolManager(cert_reqs='CERT_REQUIRED')
    merged_data = OrderedDict()
    merged_data.update(data)
    merged_data.update(files)
    response = http.request(
        'POST',
        url,
        fields=merged_data,
    )
    return response


def setup_module():
    s3 = botocore.session.get_session().create_client('s3')
    waiter = s3.get_waiter('bucket_exists')
    params = {
        'Bucket': _SHARED_BUCKET,
        'CreateBucketConfiguration': {
            'LocationConstraint': _DEFAULT_REGION,
        },
        'ObjectOwnership': 'ObjectWriter',
    }
    try:
        s3.create_bucket(**params)
    except Exception as e:
        # A create_bucket can fail for a number of reasons.
        # We're going to defer to the waiter below to make the
        # final call as to whether or not the bucket exists.
        LOG.debug("create_bucket() raised an exception: %s", e, exc_info=True)
    waiter.wait(Bucket=_SHARED_BUCKET)
    s3.delete_public_access_block(Bucket=_SHARED_BUCKET)


def clear_out_bucket(bucket, region, delete_bucket=False):
    s3 = botocore.session.get_session().create_client('s3', region_name=region)
    # Ensure the bucket exists before attempting to wipe it out
    exists_waiter = s3.get_waiter('bucket_exists')
    exists_waiter.wait(Bucket=bucket)
    page = s3.get_paginator('list_objects')
    # Use pages paired with batch delete_objects().
    for page in page.paginate(Bucket=bucket):
        keys = [{'Key': obj['Key']} for obj in page.get('Contents', [])]
        if keys:
            s3.delete_objects(Bucket=bucket, Delete={'Objects': keys})
    if delete_bucket:
        for _ in range(5):
            try:
                s3.delete_bucket(Bucket=bucket)
                break
            except s3.exceptions.NoSuchBucket:
                exists_waiter.wait(Bucket=bucket)
            except Exception as e:
                # We can sometimes get exceptions when trying to
                # delete a bucket.  We'll let the waiter make
                # the final call as to whether the bucket was able
                # to be deleted.
                LOG.debug(
                    "delete_bucket() raised an exception: %s", e, exc_info=True
                )
                not_exists_waiter = s3.get_waiter('bucket_not_exists')
                not_exists_waiter.wait(Bucket=bucket)
            except WaiterError:
                continue


def teardown_module():
    clear_out_bucket(_SHARED_BUCKET, _DEFAULT_REGION, delete_bucket=True)


class BaseS3ClientTest(unittest.TestCase):
    DEFAULT_DELAY = 5

    def setUp(self):
        self.bucket_name = _SHARED_BUCKET
        self.region = _DEFAULT_REGION
        clear_out_bucket(self.bucket_name, self.region)
        self.session = botocore.session.get_session()
        self.client = self.session.create_client('s3', region_name=self.region)

    def assert_status_code(self, response, status_code):
        self.assertEqual(
            response['ResponseMetadata']['HTTPStatusCode'], status_code
        )

    def create_bucket(self, region_name, bucket_name=None, client=None):
        bucket_client = client or self.client
        if bucket_name is None:
            bucket_name = random_bucketname()
        bucket_kwargs = {
            'Bucket': bucket_name,
            'ObjectOwnership': 'ObjectWriter',
        }
        if region_name != 'us-east-1':
            bucket_kwargs['CreateBucketConfiguration'] = {
                'LocationConstraint': region_name,
            }
        response = bucket_client.create_bucket(**bucket_kwargs)
        self.assert_status_code(response, 200)
        waiter = bucket_client.get_waiter('bucket_exists')
        consistency_waiter = ConsistencyWaiter(
            min_successes=3, delay=self.DEFAULT_DELAY, delay_initial_poll=True
        )
        consistency_waiter.wait(
            lambda: waiter.wait(Bucket=bucket_name) is None
        )
        bucket_client.delete_public_access_block(Bucket=bucket_name)
        self.addCleanup(clear_out_bucket, bucket_name, region_name, True)
        return bucket_name

    def create_object(self, key_name, body='foo', num_attempts=3):
        for _ in range(num_attempts):
            try:
                self.client.put_object(
                    Bucket=self.bucket_name, Key=key_name, Body=body
                )
                break
            except self.client.exceptions.NoSuchBucket:
                time.sleep(self.DEFAULT_DELAY)
        self.wait_until_key_exists(self.bucket_name, key_name)

    def make_tempdir(self):
        tempdir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, tempdir)
        return tempdir

    def wait_until_key_exists(
        self, bucket_name, key_name, extra_params=None, min_successes=3
    ):
        self._wait_for_key(
            bucket_name, key_name, extra_params, min_successes, exists=True
        )

    def wait_until_key_not_exists(
        self, bucket_name, key_name, extra_params=None, min_successes=3
    ):
        self._wait_for_key(
            bucket_name, key_name, extra_params, min_successes, exists=False
        )

    def _wait_for_key(
        self,
        bucket_name,
        key_name,
        extra_params=None,
        min_successes=3,
        exists=True,
    ):
        if exists:
            waiter = self.client.get_waiter('object_exists')
        else:
            waiter = self.client.get_waiter('object_not_exists')
        params = {'Bucket': bucket_name, 'Key': key_name}
        if extra_params is not None:
            params.update(extra_params)
        for _ in range(min_successes):
            waiter.wait(**params)

    def _check_bucket_versioning(self, bucket, enabled=True):
        client = self.session.create_client('s3', region_name=self.region)
        response = client.get_bucket_versioning(Bucket=bucket)
        status = response.get('Status')
        return status == 'Enabled' if enabled else status != 'Enabled'

    def wait_until_versioning_enabled(self, bucket, min_successes=3):
        waiter = ConsistencyWaiter(
            min_successes=min_successes,
            delay=self.DEFAULT_DELAY,
            delay_initial_poll=True,
        )
        waiter.wait(self._check_bucket_versioning, bucket)


class TestS3BaseWithBucket(BaseS3ClientTest):
    def setUp(self):
        super().setUp()
        self.caught_exceptions = []

    def create_multipart_upload(self, key_name):
        parsed = self.client.create_multipart_upload(
            Bucket=self.bucket_name, Key=key_name
        )
        upload_id = parsed['UploadId']
        self.addCleanup(
            self.client.abort_multipart_upload,
            UploadId=upload_id,
            Bucket=self.bucket_name,
            Key=key_name,
        )

    def abort_multipart_upload(self, bucket_name, key, upload_id):
        self.client.abort_multipart_upload(
            UploadId=upload_id, Bucket=self.bucket_name, Key=key
        )

    def delete_object(self, key, bucket_name):
        response = self.client.delete_object(Bucket=bucket_name, Key=key)
        self.assert_status_code(response, 204)

    def delete_bucket(self, bucket_name):
        response = self.client.delete_bucket(Bucket=bucket_name)
        self.assert_status_code(response, 204)

    def create_object_catch_exceptions(self, key_name):
        try:
            self.create_object(key_name=key_name)
        except Exception as e:
            self.caught_exceptions.append(e)

    def assert_num_uploads_found(
        self, operation, num_uploads, max_items=None, num_attempts=5
    ):
        amount_seen = None
        paginator = self.client.get_paginator(operation)
        for _ in range(num_attempts):
            pages = paginator.paginate(
                Bucket=self.bucket_name,
                PaginationConfig={'MaxItems': max_items},
            )
            iterators = pages.result_key_iters()
            self.assertEqual(len(iterators), 2)
            self.assertEqual(iterators[0].result_key.expression, 'Uploads')
            # It sometimes takes a while for all the uploads to show up,
            # especially if the upload was just created.  If we don't
            # see the expected amount, we retry up to num_attempts time
            # before failing.
            amount_seen = len(list(iterators[0]))
            if amount_seen == num_uploads:
                # Test passed.
                return
            else:
                # Sleep and try again.
                time.sleep(2)
        self.fail(
            "Expected to see {} uploads, instead saw: {}".format(
                num_uploads, amount_seen
            )
        )

    def create_client(self):
        # Even though the default signature_version is s3,
        # we're being explicit in case this ever changes.
        client_config = Config(signature_version='s3')
        return self.session.create_client(
            's3', self.region, config=client_config
        )

    def assert_can_put_object(self, body):
        client = self.create_client()
        response = client.put_object(
            Bucket=self.bucket_name, Key='foo', Body=body
        )
        self.assert_status_code(response, 200)
        self.addCleanup(
            client.delete_object, Bucket=self.bucket_name, Key='foo'
        )


class TestS3Buckets(TestS3BaseWithBucket):
    def setUp(self):
        super().setUp()

    def test_can_make_request(self):
        # Basic smoke test to ensure we can talk to s3.
        result = self.client.list_buckets()
        # Can't really assume anything about whether or not they have buckets,
        # but we can assume something about the structure of the response.
        self.assertEqual(
            sorted(list(result.keys())),
            ['Buckets', 'Owner', 'ResponseMetadata'],
        )

    def test_can_get_bucket_location(self):
        result = self.client.get_bucket_location(Bucket=self.bucket_name)
        self.assertIn('LocationConstraint', result)
        self.assertEqual(result['LocationConstraint'], self.region)


class TestS3Objects(TestS3BaseWithBucket):
    def increment_auth(self, request, **kwargs):
        self.auth_paths.append(request.auth_path)

    def test_can_delete_urlencoded_object(self):
        key_name = 'a+b/foo'
        self.create_object(key_name=key_name)
        bucket_contents = self.client.list_objects(Bucket=self.bucket_name)[
            'Contents'
        ]
        self.assertEqual(len(bucket_contents), 1)
        self.assertEqual(bucket_contents[0]['Key'], 'a+b/foo')

        subdir_contents = self.client.list_objects(
            Bucket=self.bucket_name, Prefix='a+b'
        )['Contents']
        self.assertEqual(len(subdir_contents), 1)
        self.assertEqual(subdir_contents[0]['Key'], 'a+b/foo')

        response = self.client.delete_object(
            Bucket=self.bucket_name, Key=key_name
        )
        self.assert_status_code(response, 204)

    @pytest.mark.slow
    def test_can_paginate(self):
        for i in range(5):
            key_name = 'key%s' % i
            self.create_object(key_name)
        # Eventual consistency.
        time.sleep(3)
        paginator = self.client.get_paginator('list_objects')
        generator = paginator.paginate(MaxKeys=1, Bucket=self.bucket_name)
        responses = list(generator)
        self.assertEqual(len(responses), 5, responses)
        key_names = [el['Contents'][0]['Key'] for el in responses]
        self.assertEqual(key_names, ['key0', 'key1', 'key2', 'key3', 'key4'])

    @pytest.mark.slow
    def test_can_paginate_with_page_size(self):
        for i in range(5):
            key_name = 'key%s' % i
            self.create_object(key_name)
        # Eventual consistency.
        time.sleep(3)
        paginator = self.client.get_paginator('list_objects')
        generator = paginator.paginate(
            PaginationConfig={'PageSize': 1}, Bucket=self.bucket_name
        )
        responses = list(generator)
        self.assertEqual(len(responses), 5, responses)
        data = [r for r in responses]
        key_names = [el['Contents'][0]['Key'] for el in data]
        self.assertEqual(key_names, ['key0', 'key1', 'key2', 'key3', 'key4'])

    @pytest.mark.slow
    def test_result_key_iters(self):
        for i in range(5):
            key_name = f'key/{i}/{i}'
            self.create_object(key_name)
            key_name2 = 'key/%s' % i
            self.create_object(key_name2)
        time.sleep(3)
        paginator = self.client.get_paginator('list_objects')
        generator = paginator.paginate(
            MaxKeys=2, Prefix='key/', Delimiter='/', Bucket=self.bucket_name
        )
        iterators = generator.result_key_iters()
        response = defaultdict(list)
        key_names = [i.result_key for i in iterators]
        for vals in zip_longest(*iterators):
            for k, val in zip(key_names, vals):
                response.setdefault(k.expression, [])
                response[k.expression].append(val)
        self.assertIn('Contents', response)
        self.assertIn('CommonPrefixes', response)

    @pytest.mark.slow
    def test_can_get_and_put_object(self):
        self.create_object('foobarbaz', body='body contents')
        time.sleep(3)

        data = self.client.get_object(Bucket=self.bucket_name, Key='foobarbaz')
        self.assertEqual(data['Body'].read().decode('utf-8'), 'body contents')

    def test_can_put_large_string_body_on_new_bucket(self):
        body = '*' * (5 * (1024**2))
        self.assert_can_put_object(body)

    def test_can_put_object_bytearray(self):
        body_bytes = b'*' * 1024
        body = bytearray(body_bytes)
        self.assert_can_put_object(body)

    def test_get_object_stream_wrapper(self):
        self.create_object('foobarbaz', body='body contents')
        response = self.client.get_object(
            Bucket=self.bucket_name, Key='foobarbaz'
        )
        body = response['Body']
        # Am able to set a socket timeout
        body.set_socket_timeout(10)
        self.assertEqual(body.read(amt=1).decode('utf-8'), 'b')
        self.assertEqual(body.read().decode('utf-8'), 'ody contents')

    def test_paginate_max_items(self):
        self.create_multipart_upload('foo/key1')
        self.create_multipart_upload('foo/key1')
        self.create_multipart_upload('foo/key1')
        self.create_multipart_upload('foo/key2')
        self.create_multipart_upload('foobar/key1')
        self.create_multipart_upload('foobar/key2')
        self.create_multipart_upload('bar/key1')
        self.create_multipart_upload('bar/key2')

        # Verify when we have MaxItems=None, we get back all 8 uploads.
        self.assert_num_uploads_found(
            'list_multipart_uploads', max_items=None, num_uploads=8
        )

        # Verify when we have MaxItems=1, we get back 1 upload.
        self.assert_num_uploads_found(
            'list_multipart_uploads', max_items=1, num_uploads=1
        )

        paginator = self.client.get_paginator('list_multipart_uploads')
        # Works similar with build_full_result()
        pages = paginator.paginate(
            PaginationConfig={'MaxItems': 1}, Bucket=self.bucket_name
        )
        full_result = pages.build_full_result()
        self.assertEqual(len(full_result['Uploads']), 1)

    def test_paginate_within_page_boundaries(self):
        self.create_object('a')
        self.create_object('b')
        self.create_object('c')
        self.create_object('d')
        paginator = self.client.get_paginator('list_objects')
        # First do it without a max keys so we're operating on a single page of
        # results.
        pages = paginator.paginate(
            PaginationConfig={'MaxItems': 1}, Bucket=self.bucket_name
        )
        first = pages.build_full_result()
        t1 = first['NextToken']

        pages = paginator.paginate(
            PaginationConfig={'MaxItems': 1, 'StartingToken': t1},
            Bucket=self.bucket_name,
        )
        second = pages.build_full_result()
        t2 = second['NextToken']

        pages = paginator.paginate(
            PaginationConfig={'MaxItems': 1, 'StartingToken': t2},
            Bucket=self.bucket_name,
        )
        third = pages.build_full_result()
        t3 = third['NextToken']

        pages = paginator.paginate(
            PaginationConfig={'MaxItems': 1, 'StartingToken': t3},
            Bucket=self.bucket_name,
        )
        fourth = pages.build_full_result()

        self.assertEqual(first['Contents'][-1]['Key'], 'a')
        self.assertEqual(second['Contents'][-1]['Key'], 'b')
        self.assertEqual(third['Contents'][-1]['Key'], 'c')
        self.assertEqual(fourth['Contents'][-1]['Key'], 'd')

    def test_unicode_key_put_list(self):
        # Verify we can upload a key with a unicode char and list it as well.
        key_name = '\u2713'
        self.create_object(key_name)
        parsed = self.client.list_objects(Bucket=self.bucket_name)
        self.assertEqual(len(parsed['Contents']), 1)
        self.assertEqual(parsed['Contents'][0]['Key'], key_name)
        parsed = self.client.get_object(Bucket=self.bucket_name, Key=key_name)
        self.assertEqual(parsed['Body'].read().decode('utf-8'), 'foo')

    def test_unicode_system_character(self):
        # Verify we can use a unicode system character which would normally
        # break the xml parser
        key_name = 'foo\x08'
        self.create_object(key_name)
        self.addCleanup(self.delete_object, key_name, self.bucket_name)
        parsed = self.client.list_objects(Bucket=self.bucket_name)
        self.assertEqual(len(parsed['Contents']), 1)
        self.assertEqual(parsed['Contents'][0]['Key'], key_name)

        parsed = self.client.list_objects(
            Bucket=self.bucket_name, EncodingType='url'
        )
        self.assertEqual(len(parsed['Contents']), 1)
        self.assertEqual(parsed['Contents'][0]['Key'], 'foo%08')

    def test_unicode_system_character_with_list_v2(self):
        # Verify we can use a unicode system character which would normally
        # break the xml parser
        key_name = 'foo\x08'
        self.create_object(key_name)
        self.addCleanup(self.delete_object, key_name, self.bucket_name)
        parsed = self.client.list_objects_v2(Bucket=self.bucket_name)
        self.assertEqual(len(parsed['Contents']), 1)
        self.assertEqual(parsed['Contents'][0]['Key'], key_name)

        parsed = self.client.list_objects_v2(
            Bucket=self.bucket_name, EncodingType='url'
        )
        self.assertEqual(len(parsed['Contents']), 1)
        self.assertEqual(parsed['Contents'][0]['Key'], 'foo%08')

    def test_unicode_system_character_with_list_object_versions(self):
        # Verify we can use a unicode system character which would normally
        # break the xml parser
        key_name = 'foo\x03'
        self.create_object(key_name)
        self.addCleanup(self.delete_object, key_name, self.bucket_name)
        parsed = self.client.list_object_versions(Bucket=self.bucket_name)
        self.assertEqual(len(parsed['Versions']), 1)
        self.assertEqual(parsed['Versions'][0]['Key'], key_name)

        parsed = self.client.list_object_versions(
            Bucket=self.bucket_name, EncodingType='url'
        )
        self.assertEqual(len(parsed['Versions']), 1)
        self.assertEqual(parsed['Versions'][0]['Key'], 'foo%03')

    def test_thread_safe_auth(self):
        self.auth_paths = []
        emitter = self.session.get_component('event_emitter')
        emitter.register_last('before-sign.s3', self.increment_auth)
        # This test depends on auth_path, which is only added in virtual host
        # style requests.
        config = Config(s3={'addressing_style': 'virtual'})
        self.client = self.session.create_client(
            's3', self.region, config=config
        )
        self.create_object(key_name='foo1')
        threads = []
        for i in range(10):
            t = threading.Thread(
                target=self.create_object_catch_exceptions, args=('foo%s' % i,)
            )
            t.daemon = True
            threads.append(t)
        for thread in threads:
            thread.start()
        for thread in threads:
            thread.join()
        self.assertEqual(
            self.caught_exceptions,
            [],
            "Unexpectedly caught exceptions: %s" % self.caught_exceptions,
        )
        self.assertEqual(
            len(set(self.auth_paths)),
            10,
            "Expected 10 unique auth paths, instead received: %s"
            % (self.auth_paths),
        )

    def test_non_normalized_key_paths(self):
        # The create_object method has assertEqual checks for 200 status.
        self.create_object('key./././name')
        bucket_contents = self.client.list_objects(Bucket=self.bucket_name)[
            'Contents'
        ]
        self.assertEqual(len(bucket_contents), 1)
        self.assertEqual(bucket_contents[0]['Key'], 'key./././name')


class TestS3Regions(BaseS3ClientTest):
    def setUp(self):
        super().setUp()
        self.region = 'us-west-2'
        self.client = self.session.create_client('s3', region_name=self.region)

    def test_reset_stream_on_redirects(self):
        # Create a bucket in a non classic region.
        bucket_name = self.create_bucket(self.region)
        # Then try to put a file like object to this location.
        tempdir = self.make_tempdir()
        filename = os.path.join(tempdir, 'foo')
        with open(filename, 'wb') as f:
            f.write(b'foo' * 1024)
        with open(filename, 'rb') as f:
            self.client.put_object(Bucket=bucket_name, Key='foo', Body=f)

        data = self.client.get_object(Bucket=bucket_name, Key='foo')
        self.assertEqual(data['Body'].read(), b'foo' * 1024)


class TestS3Copy(TestS3BaseWithBucket):
    def test_copy_with_quoted_char(self):
        key_name = 'a+b/foo'
        self.create_object(key_name=key_name)

        key_name2 = key_name + 'bar'
        self.client.copy_object(
            Bucket=self.bucket_name,
            Key=key_name2,
            CopySource=f'{self.bucket_name}/{key_name}',
        )

        # Now verify we can retrieve the copied object.
        data = self.client.get_object(Bucket=self.bucket_name, Key=key_name2)
        self.assertEqual(data['Body'].read().decode('utf-8'), 'foo')

    def test_copy_with_query_string(self):
        key_name = 'a+b/foo?notVersionid=bar'
        self.create_object(key_name=key_name)

        key_name2 = key_name + 'bar'
        self.client.copy_object(
            Bucket=self.bucket_name,
            Key=key_name2,
            CopySource=f'{self.bucket_name}/{key_name}',
        )

        # Now verify we can retrieve the copied object.
        data = self.client.get_object(Bucket=self.bucket_name, Key=key_name2)
        self.assertEqual(data['Body'].read().decode('utf-8'), 'foo')

    def test_can_copy_with_dict_form(self):
        key_name = 'a+b/foo?versionId=abcd'
        self.create_object(key_name=key_name)

        key_name2 = key_name + 'bar'
        self.client.copy_object(
            Bucket=self.bucket_name,
            Key=key_name2,
            CopySource={'Bucket': self.bucket_name, 'Key': key_name},
        )

        # Now verify we can retrieve the copied object.
        data = self.client.get_object(Bucket=self.bucket_name, Key=key_name2)
        self.assertEqual(data['Body'].read().decode('utf-8'), 'foo')

    def test_copy_with_s3_metadata(self):
        key_name = 'foo.txt'
        self.create_object(key_name=key_name)
        copied_key = 'copied.txt'
        parsed = self.client.copy_object(
            Bucket=self.bucket_name,
            Key=copied_key,
            CopySource=f'{self.bucket_name}/{key_name}',
            MetadataDirective='REPLACE',
            Metadata={"mykey": "myvalue", "mykey2": "myvalue2"},
        )
        self.assert_status_code(parsed, 200)


class BaseS3PresignTest(BaseS3ClientTest):
    def setup_bucket(self):
        self.key = 'myobject'
        self.create_object(key_name=self.key)


class TestS3PresignUsStandard(BaseS3PresignTest):
    def setUp(self):
        super().setUp()
        self.region = 'us-east-1'
        self.client_config = Config(region_name=self.region)
        self.client = self.session.create_client(
            's3', config=self.client_config
        )
        self.bucket_name = self.create_bucket(self.region)
        self.setup_bucket()

    def test_presign_sigv2(self):
        self.client_config.signature_version = 's3'
        self.client = self.session.create_client(
            's3', config=self.client_config
        )
        presigned_url = self.client.generate_presigned_url(
            'get_object', Params={'Bucket': self.bucket_name, 'Key': self.key}
        )
        self.assertTrue(
            presigned_url.startswith(
                'https://{}.s3.amazonaws.com/{}'.format(
                    self.bucket_name, self.key
                )
            ),
            "Host was suppose to use DNS style, instead "
            "got: %s" % presigned_url,
        )
        # Try to retrieve the object using the presigned url.
        self.assertEqual(http_get(presigned_url).data, b'foo')

    def test_presign_with_existing_query_string_values(self):
        content_disposition = 'attachment; filename=foo.txt;'
        presigned_url = self.client.generate_presigned_url(
            'get_object',
            Params={
                'Bucket': self.bucket_name,
                'Key': self.key,
                'ResponseContentDisposition': content_disposition,
            },
        )
        response = http_get(presigned_url)
        self.assertEqual(
            response.headers['Content-Disposition'], content_disposition
        )
        self.assertEqual(response.data, b'foo')

    def test_presign_sigv4(self):
        self.client_config.signature_version = 's3v4'
        self.client = self.session.create_client(
            's3', config=self.client_config
        )
        presigned_url = self.client.generate_presigned_url(
            'get_object', Params={'Bucket': self.bucket_name, 'Key': self.key}
        )
        self.assertTrue(
            presigned_url.startswith(
                'https://{}.s3.amazonaws.com/{}'.format(
                    self.bucket_name, self.key
                )
            ),
            "Host was suppose to be the us-east-1 endpoint, instead "
            "got: %s" % presigned_url,
        )
        # Try to retrieve the object using the presigned url.
        self.assertEqual(http_get(presigned_url).data, b'foo')

    def test_presign_post_sigv2(self):
        self.client_config.signature_version = 's3'
        self.client = self.session.create_client(
            's3', config=self.client_config
        )

        # Create some of the various supported conditions.
        conditions = [
            {"acl": "public-read"},
        ]

        # Create the fields that follow the policy.
        fields = {
            'acl': 'public-read',
        }

        # Retrieve the args for the presigned post.
        post_args = self.client.generate_presigned_post(
            self.bucket_name, self.key, Fields=fields, Conditions=conditions
        )

        # Make sure that the form can be posted successfully.
        files = {'file': ('baz', 'some data')}

        # Make sure the correct endpoint is being used
        self.assertTrue(
            post_args['url'].startswith(
                'https://%s.s3.amazonaws.com' % self.bucket_name
            ),
            "Host was suppose to use DNS style, instead "
            "got: %s" % post_args['url'],
        )

        # Try to retrieve the object using the presigned url.
        r = http_post(post_args['url'], data=post_args['fields'], files=files)
        self.assertEqual(r.status, 204)

    def test_presign_post_sigv4(self):
        self.client_config.signature_version = 's3v4'
        self.client = self.session.create_client(
            's3', config=self.client_config
        )

        # Create some of the various supported conditions.
        conditions = [
            {"acl": 'public-read'},
        ]

        # Create the fields that follow the policy.
        fields = {
            'acl': 'public-read',
        }

        # Retrieve the args for the presigned post.
        post_args = self.client.generate_presigned_post(
            self.bucket_name, self.key, Fields=fields, Conditions=conditions
        )

        # Make sure that the form can be posted successfully.
        files = {'file': ('baz', 'some data')}

        # Make sure the correct endpoint is being used
        self.assertTrue(
            post_args['url'].startswith(
                'https://%s.s3.amazonaws.com/' % self.bucket_name
            ),
            "Host was suppose to use us-east-1 endpoint, instead "
            "got: %s" % post_args['url'],
        )

        r = http_post(post_args['url'], data=post_args['fields'], files=files)
        self.assertEqual(r.status, 204)


class TestS3PresignNonUsStandard(BaseS3PresignTest):
    def setUp(self):
        super().setUp()
        self.client_config = Config(region_name=self.region)
        self.client = self.session.create_client(
            's3', config=self.client_config
        )
        self.setup_bucket()

    def test_presign_sigv2(self):
        self.client_config.signature_version = 's3'
        self.client = self.session.create_client(
            's3', config=self.client_config
        )

        presigned_url = self.client.generate_presigned_url(
            'get_object', Params={'Bucket': self.bucket_name, 'Key': self.key}
        )
        self.assertTrue(
            presigned_url.startswith(
                'https://{}.s3.amazonaws.com/{}'.format(
                    self.bucket_name, self.key
                )
            ),
            "Host was suppose to use DNS style, instead "
            "got: %s" % presigned_url,
        )
        # Try to retrieve the object using the presigned url.
        self.assertEqual(http_get(presigned_url).data, b'foo')

    def test_presign_sigv4(self):
        # For a newly created bucket, you can't use virtualhosted
        # addressing and 's3v4' due to the backwards compat behavior
        # using '.s3.amazonaws.com' for anything in the AWS partition.
        # Instead you either have to use the older 's3' signature version
        # of you have to use path style addressing.  The latter is being
        # done here.
        self.client_config.signature_version = 's3v4'
        self.client_config.s3 = {'addressing_style': 'path'}
        self.client = self.session.create_client(
            's3', config=self.client_config
        )
        presigned_url = self.client.generate_presigned_url(
            'get_object', Params={'Bucket': self.bucket_name, 'Key': self.key}
        )

        self.assertTrue(
            presigned_url.startswith(
                'https://s3.us-west-2.amazonaws.com/{}/{}'.format(
                    self.bucket_name, self.key
                )
            ),
            "Host was suppose to be the us-west-2 endpoint, instead "
            "got: %s" % presigned_url,
        )
        # Try to retrieve the object using the presigned url.
        self.assertEqual(http_get(presigned_url).data, b'foo')

    def test_presign_post_sigv2(self):
        self.client_config.signature_version = 's3'
        self.client = self.session.create_client(
            's3', config=self.client_config
        )

        # Create some of the various supported conditions.
        conditions = [
            {"acl": "public-read"},
        ]

        # Create the fields that follow the policy.
        fields = {
            'acl': 'public-read',
        }

        # Retrieve the args for the presigned post.
        post_args = self.client.generate_presigned_post(
            self.bucket_name, self.key, Fields=fields, Conditions=conditions
        )

        # Make sure that the form can be posted successfully.
        files = {'file': ('baz', 'some data')}

        # Make sure the correct endpoint is being used
        self.assertTrue(
            post_args['url'].startswith(
                'https://%s.s3.amazonaws.com' % self.bucket_name
            ),
            "Host was suppose to use DNS style, instead "
            "got: %s" % post_args['url'],
        )

        r = http_post(post_args['url'], data=post_args['fields'], files=files)
        self.assertEqual(r.status, 204)

    def test_presign_post_sigv4(self):
        self.client_config.signature_version = 's3v4'
        self.client = self.session.create_client(
            's3', config=self.client_config
        )

        # Create some of the various supported conditions.
        conditions = [
            {"acl": "public-read"},
        ]

        # Create the fields that follow the policy.
        fields = {
            'acl': 'public-read',
        }

        # Retrieve the args for the presigned post.
        post_args = self.client.generate_presigned_post(
            self.bucket_name, self.key, Fields=fields, Conditions=conditions
        )

        # Make sure that the form can be posted successfully.
        files = {'file': ('baz', 'some data')}

        # Make sure the correct endpoint is being used
        self.assertTrue(
            post_args['url'].startswith(
                'https://%s.s3.amazonaws.com/' % self.bucket_name
            ),
            "Host was suppose to use DNS style, instead "
            "got: %s" % post_args['url'],
        )

        r = http_post(post_args['url'], data=post_args['fields'], files=files)
        self.assertEqual(r.status, 204)


class TestCreateBucketInOtherRegion(TestS3BaseWithBucket):
    def test_bucket_in_other_region(self):
        # This verifies expect 100-continue behavior.  We previously
        # had a bug where we did not support this behavior and trying to
        # create a bucket and immediately PutObject with a file like object
        # would actually cause errors.
        client = self.session.create_client('s3', 'us-east-1')
        with temporary_file('w') as f:
            f.write('foobarbaz' * 1024 * 1024)
            f.flush()
            with open(f.name, 'rb') as body_file:
                response = client.put_object(
                    Bucket=self.bucket_name, Key='foo.txt', Body=body_file
                )
            self.assert_status_code(response, 200)

    def test_bucket_in_other_region_using_http(self):
        client = self.session.create_client('s3', 'us-east-1', use_ssl=False)
        with temporary_file('w') as f:
            f.write('foobarbaz' * 1024 * 1024)
            f.flush()
            with open(f.name, 'rb') as body_file:
                response = client.put_object(
                    Bucket=self.bucket_name, Key='foo.txt', Body=body_file
                )
            self.assert_status_code(response, 200)


class TestS3SigV4Client(BaseS3ClientTest):
    def setUp(self):
        super().setUp()
        self.client = self.session.create_client(
            's3', self.region, config=Config(signature_version='s3v4')
        )
        self.http_stubber = ClientHTTPStubber(self.client)

    def test_can_get_bucket_location(self):
        # Even though the bucket is in us-west-2, we should still be able to
        # use the us-east-1 endpoint class to get the bucket location.
        client = self.session.create_client('s3', 'us-east-1')
        # Also keep in mind that while this test is useful, it doesn't test
        # what happens once DNS propogates which is arguably more interesting,
        # as DNS will point us to the eu-central-1 endpoint.
        response = client.get_bucket_location(Bucket=self.bucket_name)
        self.assertEqual(response['LocationConstraint'], 'us-west-2')

    def test_request_retried_for_sigv4(self):
        body = BytesIO(b"Hello world!")
        exception = ConnectionClosedError(endpoint_url='')
        self.http_stubber.responses.append(exception)
        self.http_stubber.responses.append(None)
        with self.http_stubber:
            response = self.client.put_object(
                Bucket=self.bucket_name, Key='foo.txt', Body=body
            )
            self.assert_status_code(response, 200)

    @pytest.mark.slow
    def test_paginate_list_objects_unicode(self):
        key_names = [
            'non-ascii-key-\xe4\xf6\xfc-01.txt',
            'non-ascii-key-\xe4\xf6\xfc-02.txt',
            'non-ascii-key-\xe4\xf6\xfc-03.txt',
            'non-ascii-key-\xe4\xf6\xfc-04.txt',
        ]
        for key in key_names:
            response = self.client.put_object(
                Bucket=self.bucket_name, Key=key, Body=''
            )
            self.assert_status_code(response, 200)

        list_objs_paginator = self.client.get_paginator('list_objects')
        key_refs = []
        for response in list_objs_paginator.paginate(
            Bucket=self.bucket_name, PaginationConfig={'PageSize': 2}
        ):
            for content in response['Contents']:
                key_refs.append(content['Key'])

        self.assertEqual(key_names, key_refs)

    @pytest.mark.slow
    def test_paginate_list_objects_safe_chars(self):
        key_names = [
            '-._~safe-chars-key-01.txt',
            '-._~safe-chars-key-02.txt',
            '-._~safe-chars-key-03.txt',
            '-._~safe-chars-key-04.txt',
        ]
        for key in key_names:
            response = self.client.put_object(
                Bucket=self.bucket_name, Key=key, Body=''
            )
            self.assert_status_code(response, 200)

        list_objs_paginator = self.client.get_paginator('list_objects')
        key_refs = []
        for response in list_objs_paginator.paginate(
            Bucket=self.bucket_name, PaginationConfig={'PageSize': 2}
        ):
            for content in response['Contents']:
                key_refs.append(content['Key'])

        self.assertEqual(key_names, key_refs)

    def test_create_multipart_upload(self):
        key = 'mymultipartupload'
        response = self.client.create_multipart_upload(
            Bucket=self.bucket_name, Key=key
        )
        self.assert_status_code(response, 200)
        upload_id = response['UploadId']
        self.addCleanup(
            self.client.abort_multipart_upload,
            Bucket=self.bucket_name,
            Key=key,
            UploadId=upload_id,
        )

        response = self.client.list_multipart_uploads(
            Bucket=self.bucket_name, Prefix=key
        )

        # Make sure there is only one multipart upload.
        self.assertEqual(len(response['Uploads']), 1)
        # Make sure the upload id is as expected.
        self.assertEqual(response['Uploads'][0]['UploadId'], upload_id)

    def test_can_add_double_space_metadata(self):
        # Ensure we get no sigv4 errors when we send
        # metadata with consecutive spaces.
        response = self.client.put_object(
            Bucket=self.bucket_name,
            Key='foo.txt',
            Body=b'foobar',
            Metadata={'foo': '  multi    spaces  '},
        )
        self.assert_status_code(response, 200)

    def test_bad_request_on_invalid_credentials(self):
        # A previous bug would cause this to hang.  We want
        # to verify we get the 400 response.
        # In order to test we need a key that actually
        # exists so we use the properly configured self.client.
        self.client.put_object(
            Bucket=self.bucket_name, Key='foo.txt', Body=b'asdfasdf'
        )
        # Now we create a client with a bad session token
        # which should give us a 400 response.
        creds = self.session.get_credentials()
        client = self.session.create_client(
            's3',
            self.region,
            config=Config(signature_version='s3v4'),
            aws_access_key_id=creds.access_key,
            aws_secret_access_key=creds.secret_key,
            aws_session_token='bad-token-causes-400',
        )
        with self.assertRaises(ClientError) as e:
            client.head_object(
                Bucket=self.bucket_name,
                Key='foo.txt',
            )
        self.assertEqual(e.exception.response['Error']['Code'], '400')


class TestSSEKeyParamValidation(BaseS3ClientTest):
    def test_make_request_with_sse(self):
        key_bytes = os.urandom(32)
        # Obviously a bad key here, but we just want to ensure we can use
        # a str/unicode type as a key.
        key_str = 'abcd' * 8

        # Put two objects with an sse key, one with random bytes,
        # one with str/unicode.  Then verify we can GetObject() both
        # objects.
        self.client.put_object(
            Bucket=self.bucket_name,
            Key='foo.txt',
            Body=BytesIO(b'mycontents'),
            SSECustomerAlgorithm='AES256',
            SSECustomerKey=key_bytes,
        )
        self.addCleanup(
            self.client.delete_object, Bucket=self.bucket_name, Key='foo.txt'
        )
        self.client.put_object(
            Bucket=self.bucket_name,
            Key='foo2.txt',
            Body=BytesIO(b'mycontents2'),
            SSECustomerAlgorithm='AES256',
            SSECustomerKey=key_str,
        )
        self.addCleanup(
            self.client.delete_object, Bucket=self.bucket_name, Key='foo2.txt'
        )

        self.assertEqual(
            self.client.get_object(
                Bucket=self.bucket_name,
                Key='foo.txt',
                SSECustomerAlgorithm='AES256',
                SSECustomerKey=key_bytes,
            )['Body'].read(),
            b'mycontents',
        )
        self.assertEqual(
            self.client.get_object(
                Bucket=self.bucket_name,
                Key='foo2.txt',
                SSECustomerAlgorithm='AES256',
                SSECustomerKey=key_str,
            )['Body'].read(),
            b'mycontents2',
        )

    def test_make_request_with_sse_copy_source(self):
        encrypt_key = 'a' * 32
        other_encrypt_key = 'b' * 32

        # Upload the object using one encrypt key
        self.client.put_object(
            Bucket=self.bucket_name,
            Key='foo.txt',
            Body=BytesIO(b'mycontents'),
            SSECustomerAlgorithm='AES256',
            SSECustomerKey=encrypt_key,
        )
        self.addCleanup(
            self.client.delete_object, Bucket=self.bucket_name, Key='foo.txt'
        )

        # Copy the object using the original encryption key as the copy source
        # and encrypt with a new encryption key.
        self.client.copy_object(
            Bucket=self.bucket_name,
            CopySource=self.bucket_name + '/foo.txt',
            Key='bar.txt',
            CopySourceSSECustomerAlgorithm='AES256',
            CopySourceSSECustomerKey=encrypt_key,
            SSECustomerAlgorithm='AES256',
            SSECustomerKey=other_encrypt_key,
        )
        self.addCleanup(
            self.client.delete_object, Bucket=self.bucket_name, Key='bar.txt'
        )

        # Download the object using the new encryption key.
        # The content should not have changed.
        self.assertEqual(
            self.client.get_object(
                Bucket=self.bucket_name,
                Key='bar.txt',
                SSECustomerAlgorithm='AES256',
                SSECustomerKey=other_encrypt_key,
            )['Body'].read(),
            b'mycontents',
        )


class TestS3UTF8Headers(BaseS3ClientTest):
    def test_can_set_utf_8_headers(self):
        bucket_name = _SHARED_BUCKET
        body = BytesIO(b"Hello world!")
        response = self.client.put_object(
            Bucket=bucket_name,
            Key="foo.txt",
            Body=body,
            ContentDisposition="attachment; filename=5小時接力起跑.jpg;",
        )
        self.assert_status_code(response, 200)
        self.addCleanup(
            self.client.delete_object, Bucket=bucket_name, Key="foo.txt"
        )


class TestSupportedPutObjectBodyTypes(TestS3BaseWithBucket):
    def test_can_put_unicode_content(self):
        self.assert_can_put_object(body='\u2713')

    def test_can_put_non_ascii_bytes(self):
        self.assert_can_put_object(body='\u2713'.encode())

    def test_can_put_arbitrary_binary_data(self):
        body = os.urandom(5 * (1024**2))
        self.assert_can_put_object(body)

    def test_can_put_binary_file(self):
        tempdir = self.make_tempdir()
        filename = os.path.join(tempdir, 'foo')
        with open(filename, 'wb') as f:
            f.write('\u2713'.encode())
        with open(filename, 'rb') as binary_file:
            self.assert_can_put_object(body=binary_file)

    def test_can_put_extracted_file_from_tar(self):
        tempdir = self.make_tempdir()
        tarname = os.path.join(tempdir, 'mytar.tar')
        filename = os.path.join(tempdir, 'foo')

        # Set up a file to add the tarfile.
        with open(filename, 'w') as f:
            f.write('bar')

        # Setup the tar file by adding the file to it.
        # Note there is no context handler for TarFile in python 2.6
        try:
            tar = TarFile(tarname, 'w')
            tar.add(filename, 'foo')
        finally:
            tar.close()

        # See if an extracted file can be uploaded to s3.
        try:
            tar = TarFile(tarname, 'r')
            with closing(tar.extractfile('foo')) as f:
                self.assert_can_put_object(body=f)
        finally:
            tar.close()


class TestSupportedPutObjectBodyTypesSigv4(TestSupportedPutObjectBodyTypes):
    def create_client(self):
        client_config = Config(signature_version='s3v4')
        return self.session.create_client(
            's3', self.region, config=client_config
        )


class TestAutoS3Addressing(BaseS3ClientTest):
    def setUp(self):
        super().setUp()
        self.addressing_style = 'auto'
        self.client = self.create_client()

    def create_client(self, signature_version='s3'):
        return self.session.create_client(
            's3',
            region_name=self.region,
            config=Config(
                s3={
                    'addressing_style': self.addressing_style,
                    'signature_version': signature_version,
                }
            ),
        )

    def test_can_list_buckets(self):
        response = self.client.list_buckets()
        self.assertIn('Buckets', response)

    def test_can_make_bucket_and_put_object(self):
        response = self.client.put_object(
            Bucket=self.bucket_name, Key='foo', Body='contents'
        )
        self.assertEqual(response['ResponseMetadata']['HTTPStatusCode'], 200)

    def test_can_make_bucket_and_put_object_with_sigv4(self):
        self.region = 'eu-central-1'
        self.client = self.create_client()
        bucket_name = self.create_bucket(self.region)
        response = self.client.put_object(
            Bucket=bucket_name, Key='foo', Body='contents'
        )
        self.assertEqual(response['ResponseMetadata']['HTTPStatusCode'], 200)


class TestS3VirtualAddressing(TestAutoS3Addressing):
    def setUp(self):
        super().setUp()
        self.addressing_style = 'virtual'
        self.client = self.create_client()


class TestS3PathAddressing(TestAutoS3Addressing):
    def setUp(self):
        super().setUp()
        self.addressing_style = 'path'
        self.client = self.create_client()


class TestRegionRedirect(BaseS3ClientTest):
    def setUp(self):
        super().setUp()
        self.bucket_region = self.region
        self.client_region = 'eu-central-1'

        self.client = self.session.create_client(
            's3',
            region_name=self.client_region,
            config=Config(signature_version='s3v4'),
        )

        self.bucket_client = self.session.create_client(
            's3',
            region_name=self.bucket_region,
            config=Config(signature_version='s3v4'),
        )

    def test_region_redirects(self):
        try:
            response = self.client.list_objects(Bucket=self.bucket_name)
            self.assertEqual(
                response['ResponseMetadata']['HTTPStatusCode'], 200
            )
        except ClientError as e:
            error = e.response['Error'].get('Code', None)
            if error == 'PermanentRedirect':
                self.fail("S3 client failed to redirect to the proper region.")

    def test_region_redirect_sigv2_to_sigv4_raises_error(self):
        self.bucket_region = 'eu-central-1'
        sigv2_client = self.session.create_client(
            's3',
            region_name=self.client_region,
            config=Config(signature_version='s3'),
        )

        eu_bucket = self.create_bucket(self.bucket_region)
        msg = 'The authorization mechanism you have provided is not supported.'
        with self.assertRaisesRegex(ClientError, msg):
            sigv2_client.list_objects(Bucket=eu_bucket)

    def test_region_redirects_multiple_requests(self):
        try:
            response = self.client.list_objects(Bucket=self.bucket_name)
            self.assertEqual(
                response['ResponseMetadata']['HTTPStatusCode'], 200
            )
            second_response = self.client.list_objects(Bucket=self.bucket_name)
            self.assertEqual(
                second_response['ResponseMetadata']['HTTPStatusCode'], 200
            )
        except ClientError as e:
            error = e.response['Error'].get('Code', None)
            if error == 'PermanentRedirect':
                self.fail("S3 client failed to redirect to the proper region.")

    def test_redirects_head_bucket(self):
        response = self.client.head_bucket(Bucket=self.bucket_name)
        headers = response['ResponseMetadata']['HTTPHeaders']
        region = headers.get('x-amz-bucket-region')
        self.assertEqual(region, self.bucket_region)

    def test_redirects_head_object(self):
        key = 'foo'
        self.bucket_client.put_object(
            Bucket=self.bucket_name, Key=key, Body='bar'
        )
        self.wait_until_key_exists(self.bucket_name, key)
        try:
            response = self.client.head_object(
                Bucket=self.bucket_name, Key=key
            )
            self.assertEqual(response.get('ContentLength'), len(key))
        except ClientError as e:
            self.fail("S3 Client failed to redirect Head Object: %s" % e)


class TestBucketWithVersions(BaseS3ClientTest):
    def extract_version_ids(self, versions):
        version_ids = []
        for marker in versions['DeleteMarkers']:
            version_ids.append(marker['VersionId'])
        for version in versions['Versions']:
            version_ids.append(version['VersionId'])
        return version_ids

    def test_create_versioned_bucket(self):
        # Verifies we can:
        # 1. Create a bucket
        # 2. Enable versioning
        # 3. Put an Object
        bucket = self.create_bucket(self.region)

        self.client.put_bucket_versioning(
            Bucket=bucket,
            VersioningConfiguration={"Status": "Enabled"},
        )
        self.wait_until_versioning_enabled(bucket)

        key = 'testkey'
        body = b'bytes body'
        response = self.client.put_object(Bucket=bucket, Key=key, Body=body)
        self.addCleanup(
            self.client.delete_object,
            Bucket=bucket,
            Key=key,
            VersionId=response['VersionId'],
        )
        self.wait_until_key_exists(bucket, key)

        response = self.client.get_object(Bucket=bucket, Key=key)
        self.assertEqual(response['Body'].read(), body)

        response = self.client.delete_object(Bucket=bucket, Key=key)
        # This cleanup step removes the DeleteMarker that's created
        # from the delete_object call above.
        self.addCleanup(
            self.client.delete_object,
            Bucket=bucket,
            Key=key,
            VersionId=response['VersionId'],
        )
        # Object does not exist anymore.
        with self.assertRaises(ClientError):
            self.client.get_object(Bucket=bucket, Key=key)
        versions = self.client.list_object_versions(Bucket=bucket)
        version_ids = self.extract_version_ids(versions)
        self.assertEqual(len(version_ids), 2)
