예제 #1
0
    def handle_put(self, req):
        """
        Handle put request when it contains X-Symlink-Target header.

        Symlink headers are validated and moved to sysmeta namespace.
        :param req: HTTP PUT object request
        :returns: Response Iterator
        """
        if req.content_length is None:
            has_body = (req.body_file.read(1) != b'')
        else:
            has_body = (req.content_length != 0)
        if has_body:
            raise HTTPBadRequest(
                body='Symlink requests require a zero byte body',
                request=req,
                content_type='text/plain')

        symlink_target_path, etag = _validate_and_prep_request_headers(req)
        if etag:
            self._validate_etag_and_update_sysmeta(
                req, symlink_target_path, etag)
        # N.B. TGT_ETAG_SYMLINK_HDR was converted as part of verifying it
        symlink_usermeta_to_sysmeta(req.headers)
        # Store info in container update that this object is a symlink.
        # We have a design decision to use etag space to store symlink info for
        # object listing because it's immutable unless the object is
        # overwritten. This may impact the downgrade scenario that the symlink
        # info can appear as the suffix in the hash value of object
        # listing result for clients.
        # To create override etag easily, we have a constraint that the symlink
        # must be 0 byte so we can add etag of the empty string + symlink info
        # here, simply (if no other override etag was provided). Note that this
        # override etag may be encrypted in the container db by encryption
        # middleware.

        etag_override = [
            req.headers.get(get_container_update_override_key('etag'),
                            MD5_OF_EMPTY_STRING),
            'symlink_target=%s' % req.headers[TGT_OBJ_SYSMETA_SYMLINK_HDR]
        ]
        if TGT_ACCT_SYSMETA_SYMLINK_HDR in req.headers:
            etag_override.append(
                'symlink_target_account=%s' %
                req.headers[TGT_ACCT_SYSMETA_SYMLINK_HDR])
        if TGT_ETAG_SYSMETA_SYMLINK_HDR in req.headers:
            # if _validate_etag_and_update_sysmeta or a middleware sets
            # TGT_ETAG_SYSMETA_SYMLINK_HDR then they need to also set
            # TGT_BYTES_SYSMETA_SYMLINK_HDR.  If they forget, they get a
            # KeyError traceback and client gets a ServerError
            etag_override.extend([
                'symlink_target_etag=%s' %
                req.headers[TGT_ETAG_SYSMETA_SYMLINK_HDR],
                'symlink_target_bytes=%s' %
                req.headers[TGT_BYTES_SYSMETA_SYMLINK_HDR],
            ])
        req.headers[get_container_update_override_key('etag')] = \
            '; '.join(etag_override)

        return self._app_call(req.environ)
예제 #2
0
    def decrypt_resp_headers(self, put_keys, post_keys):
        """
        Find encrypted headers and replace with the decrypted versions.

        :param put_keys: a dict of decryption keys used for object PUT.
        :param post_keys: a dict of decryption keys used for object POST.

        :return: A list of headers with any encrypted headers replaced by their
                 decrypted values.
        :raises HTTPInternalServerError: if any error occurs while decrypting
                                         headers
        :raises HTTPForbidden: if the decryption key is invalid
        """
        mod_hdr_pairs = []

        if put_keys:
            # Decrypt plaintext etag and place in Etag header for client
            # response
            etag_header = 'X-Object-Sysmeta-Crypto-Etag'
            encrypted_etag = self._response_header_value(etag_header)
            decrypted_etag = None
            if encrypted_etag and 'object' in put_keys:
                decrypted_etag = self._decrypt_header(etag_header,
                                                      encrypted_etag,
                                                      put_keys['object'],
                                                      required=True)
                mod_hdr_pairs.append(('Etag', decrypted_etag))

            etag_header = get_container_update_override_key('etag')
            encrypted_etag = self._response_header_value(etag_header)
            if encrypted_etag and 'container' in put_keys:
                dcrypt_etag_override = self._decrypt_header(
                    etag_header, encrypted_etag, put_keys['container'])
                if decrypted_etag and dcrypt_etag_override != decrypted_etag:
                    self.app.logger.debug('Failed ETag verification')
                    raise HTTPForbidden('Invalid key')
                mod_hdr_pairs.append((etag_header, dcrypt_etag_override))
                # The real swift saves the cyphered ETag in the 'ETag' field,
                # whereas we store the ETag of the cyphered object.
                # The ETag of the cyphered object is of no use for previous
                # middlewares, so we replace it with the plaintext ETag.
                mod_hdr_pairs.append(('ETag', dcrypt_etag_override))

        # Decrypt all user metadata. Encrypted user metadata values are stored
        # in the x-object-transient-sysmeta-crypto-meta- namespace. Those are
        # decrypted and moved back to the x-object-meta- namespace. Prior to
        # decryption, the response should have no x-object-meta- headers, but
        # if it does then they will be overwritten by any decrypted headers
        # that map to the same x-object-meta- header names i.e. decrypted
        # headers win over unexpected, unencrypted headers.
        try:
            if post_keys:
                mod_hdr_pairs.extend(self.decrypt_user_metadata(post_keys))

            mod_hdr_names = {h.lower() for h, v in mod_hdr_pairs}
            mod_hdr_pairs.extend([(h, v) for h, v in self._response_headers
                                  if h.lower() not in mod_hdr_names])
        except KeyError:
            self.app.logger.debug('Not able to decrypt user metadata')
        return mod_hdr_pairs
예제 #3
0
    def _validate_etag_and_update_sysmeta(self, req, symlink_target_path,
                                          etag):
        if req.environ.get('swift.symlink_override'):
            req.headers[TGT_ETAG_SYSMETA_SYMLINK_HDR] = etag
            req.headers[TGT_BYTES_SYSMETA_SYMLINK_HDR] = \
                req.headers[TGT_BYTES_SYMLINK_HDR]
            return

        # next we'll make sure the E-Tag matches a real object
        new_req = make_subrequest(req.environ,
                                  path=wsgi_quote(symlink_target_path),
                                  method='HEAD',
                                  swift_source='SYM')
        if req.allow_reserved_names:
            new_req.headers['X-Backend-Allow-Reserved-Names'] = 'true'
        self._last_target_path = symlink_target_path
        resp = self._recursive_get_head(new_req,
                                        target_etag=etag,
                                        follow_softlinks=False)
        if self._get_status_int() == HTTP_NOT_FOUND:
            raise HTTPConflict(body='X-Symlink-Target does not exist',
                               request=req,
                               headers={
                                   'Content-Type': 'text/plain',
                                   'Content-Location': self._last_target_path
                               })
        if not is_success(self._get_status_int()):
            drain_and_close(resp)
            raise status_map[self._get_status_int()](request=req)
        response_headers = HeaderKeyDict(self._response_headers)
        # carry forward any etag update params (e.g. "slo_etag"), we'll append
        # symlink_target_* params to this header after this method returns
        override_header = get_container_update_override_key('etag')
        if override_header in response_headers and \
                override_header not in req.headers:
            sep, params = response_headers[override_header].partition(';')[1:]
            req.headers[override_header] = MD5_OF_EMPTY_STRING + sep + params

        # It's troublesome that there's so much leakage with SLO
        if 'X-Object-Sysmeta-Slo-Etag' in response_headers and \
                override_header not in req.headers:
            req.headers[override_header] = '%s; slo_etag=%s' % (
                MD5_OF_EMPTY_STRING,
                response_headers['X-Object-Sysmeta-Slo-Etag'])
        req.headers[TGT_BYTES_SYSMETA_SYMLINK_HDR] = (
            response_headers.get('x-object-sysmeta-slo-size')
            or response_headers['Content-Length'])

        req.headers[TGT_ETAG_SYSMETA_SYMLINK_HDR] = etag

        if not req.headers.get('Content-Type'):
            req.headers['Content-Type'] = response_headers['Content-Type']
예제 #4
0
    def decrypt_resp_headers(self, put_keys, post_keys):
        """
        Find encrypted headers and replace with the decrypted versions.

        :param put_keys: a dict of decryption keys used for object PUT.
        :param post_keys: a dict of decryption keys used for object POST.
        :return: A list of headers with any encrypted headers replaced by their
                 decrypted values.
        :raises HTTPInternalServerError: if any error occurs while decrypting
                                         headers
        """
        mod_hdr_pairs = []

        if put_keys:
            # Decrypt plaintext etag and place in Etag header for client
            # response
            etag_header = 'X-Object-Sysmeta-Crypto-Etag'
            encrypted_etag = self._response_header_value(etag_header)
            if encrypted_etag:
                decrypted_etag = self._decrypt_header(
                    etag_header, encrypted_etag, put_keys['object'],
                    required=True)
                mod_hdr_pairs.append(('Etag', decrypted_etag))

            etag_header = get_container_update_override_key('etag')
            encrypted_etag = self._response_header_value(etag_header)
            if encrypted_etag:
                decrypted_etag = self._decrypt_header(
                    etag_header, encrypted_etag, put_keys['container'])
                mod_hdr_pairs.append((etag_header, decrypted_etag))

        # Decrypt all user metadata. Encrypted user metadata values are stored
        # in the x-object-transient-sysmeta-crypto-meta- namespace. Those are
        # decrypted and moved back to the x-object-meta- namespace. Prior to
        # decryption, the response should have no x-object-meta- headers, but
        # if it does then they will be overwritten by any decrypted headers
        # that map to the same x-object-meta- header names i.e. decrypted
        # headers win over unexpected, unencrypted headers.
        if post_keys:
            mod_hdr_pairs.extend(self.decrypt_user_metadata(post_keys))

        mod_hdr_names = {h.lower() for h, v in mod_hdr_pairs}
        mod_hdr_pairs.extend([(h, v) for h, v in self._response_headers
                              if h.lower() not in mod_hdr_names])
        return mod_hdr_pairs
예제 #5
0
    def handle_put(self, req):
        """
        Handle put request when it contains X-Symlink-Target header.

        Symlink headers are validated and moved to sysmeta namespace.
        :param req: HTTP PUT object request
        :returns: Response Iterator
        """
        if req.content_length is None:
            has_body = (req.body_file.read(1) != b'')
        else:
            has_body = (req.content_length != 0)
        if has_body:
            raise HTTPBadRequest(
                body='Symlink requests require a zero byte body',
                request=req,
                content_type='text/plain')

        _check_symlink_header(req)
        symlink_usermeta_to_sysmeta(req.headers)
        # Store info in container update that this object is a symlink.
        # We have a design decision to use etag space to store symlink info for
        # object listing because it's immutable unless the object is
        # overwritten. This may impact the downgrade scenario that the symlink
        # info can appear as the suffix in the hash value of object
        # listing result for clients.
        # To create override etag easily, we have a constraint that the symlink
        # must be 0 byte so we can add etag of the empty string + symlink info
        # here, simply. Note that this override etag may be encrypted in the
        # container db by encryption middleware.
        etag_override = [
            MD5_OF_EMPTY_STRING,
            'symlink_target=%s' % req.headers[TGT_OBJ_SYSMETA_SYMLINK_HDR]
        ]
        if TGT_ACCT_SYSMETA_SYMLINK_HDR in req.headers:
            etag_override.append('symlink_target_account=%s' %
                                 req.headers[TGT_ACCT_SYSMETA_SYMLINK_HDR])
        req.headers[get_container_update_override_key('etag')] = \
            '; '.join(etag_override)

        return self._app_call(req.environ)
예제 #6
0
    def POST(self, req):
        """
        Handles Complete Multipart Upload.
        """
        upload_id = req.params['uploadId']
        resp = _get_upload_info(req, self.app, upload_id)
        headers = {
            'Accept': 'application/json',
            sysmeta_header('object', 'upload-id'): upload_id
        }
        for key, val in resp.headers.items():
            _key = key.lower()
            if _key.startswith('x-amz-meta-'):
                headers['x-object-meta-' + _key[11:]] = val

        hct_header = sysmeta_header('object', 'has-content-type')
        if resp.sysmeta_headers.get(hct_header) == 'yes':
            content_type = resp.sysmeta_headers.get(
                sysmeta_header('object', 'content-type'))
        elif hct_header in resp.sysmeta_headers:
            # has-content-type is present but false, so no content type was
            # set on initial upload. In that case, we won't set one on our
            # PUT request. Swift will end up guessing one based on the
            # object name.
            content_type = None
        else:
            content_type = resp.headers.get('Content-Type')

        if content_type:
            headers['Content-Type'] = content_type

        container = req.container_name + MULTIUPLOAD_SUFFIX
        s3_etag_hasher = md5(usedforsecurity=False)
        manifest = []
        previous_number = 0
        try:
            xml = req.xml(MAX_COMPLETE_UPLOAD_BODY_SIZE)
            if not xml:
                raise InvalidRequest(msg='You must specify at least one part')
            if 'content-md5' in req.headers:
                # If an MD5 was provided, we need to verify it.
                # Note that S3Request already took care of translating to ETag
                if req.headers['etag'] != md5(
                        xml, usedforsecurity=False).hexdigest():
                    raise BadDigest(content_md5=req.headers['content-md5'])
                # We're only interested in the body here, in the
                # multipart-upload controller -- *don't* let it get
                # plumbed down to the object-server
                del req.headers['etag']

            complete_elem = fromstring(xml, 'CompleteMultipartUpload',
                                       self.logger)
            for part_elem in complete_elem.iterchildren('Part'):
                part_number = int(part_elem.find('./PartNumber').text)

                if part_number <= previous_number:
                    raise InvalidPartOrder(upload_id=upload_id)
                previous_number = part_number

                etag = normalize_etag(part_elem.find('./ETag').text)
                if len(etag) != 32 or any(c not in '0123456789abcdef'
                                          for c in etag):
                    raise InvalidPart(upload_id=upload_id,
                                      part_number=part_number)

                manifest.append({
                    'path':
                    '/%s/%s/%s/%d' %
                    (wsgi_to_str(container), wsgi_to_str(
                        req.object_name), upload_id, part_number),
                    'etag':
                    etag
                })
                s3_etag_hasher.update(binascii.a2b_hex(etag))
        except (XMLSyntaxError, DocumentInvalid):
            # NB: our schema definitions catch uploads with no parts here
            raise MalformedXML()
        except ErrorResponse:
            raise
        except Exception as e:
            self.logger.error(e)
            raise

        s3_etag = '%s-%d' % (s3_etag_hasher.hexdigest(), len(manifest))
        s3_etag_header = sysmeta_header('object', 'etag')
        if resp.sysmeta_headers.get(s3_etag_header) == s3_etag:
            # This header should only already be present if the upload marker
            # has been cleaned up and the current target uses the same
            # upload-id; assuming the segments to use haven't changed, the work
            # is already done
            return HTTPOk(body=_make_complete_body(req, s3_etag, False),
                          content_type='application/xml')
        headers[s3_etag_header] = s3_etag
        # Leave base header value blank; SLO will populate
        c_etag = '; s3_etag=%s' % s3_etag
        headers[get_container_update_override_key('etag')] = c_etag

        too_small_message = ('s3api requires that each segment be at least '
                             '%d bytes' % self.conf.min_segment_size)

        def size_checker(manifest):
            # Check the size of each segment except the last and make sure
            # they are all more than the minimum upload chunk size.
            # Note that we need to use the *internal* keys, since we're
            # looking at the manifest that's about to be written.
            return [(item['name'], too_small_message) for item in manifest[:-1]
                    if item and item['bytes'] < self.conf.min_segment_size]

        req.environ['swift.callback.slo_manifest_hook'] = size_checker
        start_time = time.time()

        def response_iter():
            # NB: XML requires that the XML declaration, if present, be at the
            # very start of the document. Clients *will* call us out on not
            # being valid XML if we pass through whitespace before it.
            # Track whether we've sent anything yet so we can yield out that
            # declaration *first*
            yielded_anything = False

            try:
                try:
                    # TODO: add support for versioning
                    put_resp = req.get_response(self.app,
                                                'PUT',
                                                body=json.dumps(manifest),
                                                query={
                                                    'multipart-manifest':
                                                    'put',
                                                    'heartbeat': 'on'
                                                },
                                                headers=headers)
                    if put_resp.status_int == 202:
                        body = []
                        put_resp.fix_conditional_response()
                        for chunk in put_resp.response_iter:
                            if not chunk.strip():
                                if time.time() - start_time < 10:
                                    # Include some grace period to keep
                                    # ceph-s3tests happy
                                    continue
                                if not yielded_anything:
                                    yield (b'<?xml version="1.0" '
                                           b'encoding="UTF-8"?>\n')
                                yielded_anything = True
                                yield chunk
                                continue
                            body.append(chunk)
                        body = json.loads(b''.join(body))
                        if body['Response Status'] != '201 Created':
                            for seg, err in body['Errors']:
                                if err == too_small_message:
                                    raise EntityTooSmall()
                                elif err in ('Etag Mismatch', '404 Not Found'):
                                    raise InvalidPart(upload_id=upload_id)
                            raise InvalidRequest(
                                status=body['Response Status'],
                                msg='\n'.join(': '.join(err)
                                              for err in body['Errors']))
                except BadSwiftRequest as e:
                    msg = str(e)
                    if too_small_message in msg:
                        raise EntityTooSmall(msg)
                    elif ', Etag Mismatch' in msg:
                        raise InvalidPart(upload_id=upload_id)
                    elif ', 404 Not Found' in msg:
                        raise InvalidPart(upload_id=upload_id)
                    else:
                        raise

                # clean up the multipart-upload record
                obj = '%s/%s' % (req.object_name, upload_id)
                try:
                    req.get_response(self.app, 'DELETE', container, obj)
                except NoSuchKey:
                    # The important thing is that we wrote out a tombstone to
                    # make sure the marker got cleaned up. If it's already
                    # gone (e.g., because of concurrent completes or a retried
                    # complete), so much the better.
                    pass

                yield _make_complete_body(req, s3_etag, yielded_anything)
            except ErrorResponse as err_resp:
                if yielded_anything:
                    err_resp.xml_declaration = False
                    yield b'\n'
                else:
                    # Oh good, we can still change HTTP status code, too!
                    resp.status = err_resp.status
                for chunk in err_resp({}, lambda *a: None):
                    yield chunk

        resp = HTTPOk()  # assume we're good for now... but see above!
        resp.app_iter = reiterate(response_iter())
        resp.content_type = "application/xml"

        return resp
예제 #7
0
    def PUT(self, req):
        """
        Handles Upload Part and Upload Part Copy.
        """

        if 'uploadId' not in req.params:
            raise InvalidArgument('ResourceType', 'partNumber',
                                  'Unexpected query string parameter')

        try:
            part_number = int(req.params['partNumber'])
            if part_number < 1 or self.conf.max_upload_part_num < part_number:
                raise Exception()
        except Exception:
            err_msg = 'Part number must be an integer between 1 and %d,' \
                      ' inclusive' % self.conf.max_upload_part_num
            raise InvalidArgument('partNumber', req.params['partNumber'],
                                  err_msg)

        upload_id = req.params['uploadId']
        _get_upload_info(req, self.app, upload_id)

        req.container_name += MULTIUPLOAD_SUFFIX
        req.object_name = '%s/%s/%d' % (req.object_name, upload_id,
                                        part_number)

        req_timestamp = S3Timestamp.now()
        req.headers['X-Timestamp'] = req_timestamp.internal
        source_resp = req.check_copy_source(self.app)
        if 'X-Amz-Copy-Source' in req.headers and \
                'X-Amz-Copy-Source-Range' in req.headers:
            rng = req.headers['X-Amz-Copy-Source-Range']

            header_valid = True
            try:
                rng_obj = Range(rng)
                if len(rng_obj.ranges) != 1:
                    header_valid = False
            except ValueError:
                header_valid = False
            if not header_valid:
                err_msg = ('The x-amz-copy-source-range value must be of the '
                           'form bytes=first-last where first and last are '
                           'the zero-based offsets of the first and last '
                           'bytes to copy')
                raise InvalidArgument('x-amz-source-range', rng, err_msg)

            source_size = int(source_resp.headers['Content-Length'])
            if not rng_obj.ranges_for_length(source_size):
                err_msg = ('Range specified is not valid for source object '
                           'of size: %s' % source_size)
                raise InvalidArgument('x-amz-source-range', rng, err_msg)

            req.headers['Range'] = rng
            del req.headers['X-Amz-Copy-Source-Range']
        if 'X-Amz-Copy-Source' in req.headers:
            # Clear some problematic headers that might be on the source
            req.headers.update({
                sysmeta_header('object', 'etag'): '',
                'X-Object-Sysmeta-Swift3-Etag': '',  # for legacy data
                'X-Object-Sysmeta-Slo-Etag': '',
                'X-Object-Sysmeta-Slo-Size': '',
                get_container_update_override_key('etag'): '',
            })
        resp = req.get_response(self.app)

        if 'X-Amz-Copy-Source' in req.headers:
            resp.append_copy_resp_body(req.controller_name,
                                       req_timestamp.s3xmlformat)

        resp.status = 200
        return resp
예제 #8
0
    def POST(self, req):
        """
        Handles Complete Multipart Upload.
        """
        upload_id = req.params['uploadId']
        resp = _get_upload_info(req, self.app, upload_id)
        headers = {'Accept': 'application/json'}
        for key, val in resp.headers.items():
            _key = key.lower()
            if _key.startswith('x-amz-meta-'):
                headers['x-object-meta-' + _key[11:]] = val

        hct_header = sysmeta_header('object', 'has-content-type')
        if resp.sysmeta_headers.get(hct_header) == 'yes':
            content_type = resp.sysmeta_headers.get(
                sysmeta_header('object', 'content-type'))
        elif hct_header in resp.sysmeta_headers:
            # has-content-type is present but false, so no content type was
            # set on initial upload. In that case, we won't set one on our
            # PUT request. Swift will end up guessing one based on the
            # object name.
            content_type = None
        else:
            content_type = resp.headers.get('Content-Type')

        if content_type:
            headers['Content-Type'] = content_type

        container = req.container_name + MULTIUPLOAD_SUFFIX
        s3_etag_hasher = md5()
        manifest = []
        previous_number = 0
        try:
            xml = req.xml(MAX_COMPLETE_UPLOAD_BODY_SIZE)
            if not xml:
                raise InvalidRequest(msg='You must specify at least one part')
            if 'content-md5' in req.headers:
                # If an MD5 was provided, we need to verify it.
                # Note that S3Request already took care of translating to ETag
                if req.headers['etag'] != md5(xml).hexdigest():
                    raise BadDigest(content_md5=req.headers['content-md5'])
                # We're only interested in the body here, in the
                # multipart-upload controller -- *don't* let it get
                # plumbed down to the object-server
                del req.headers['etag']

            complete_elem = fromstring(xml, 'CompleteMultipartUpload',
                                       self.logger)
            for part_elem in complete_elem.iterchildren('Part'):
                part_number = int(part_elem.find('./PartNumber').text)

                if part_number <= previous_number:
                    raise InvalidPartOrder(upload_id=upload_id)
                previous_number = part_number

                etag = normalize_etag(part_elem.find('./ETag').text)
                if len(etag) != 32 or any(c not in '0123456789abcdef'
                                          for c in etag):
                    raise InvalidPart(upload_id=upload_id,
                                      part_number=part_number)

                manifest.append({
                    'path':
                    '/%s/%s/%s/%d' %
                    (container, req.object_name, upload_id, part_number),
                    'etag':
                    etag
                })
                s3_etag_hasher.update(binascii.a2b_hex(etag))
        except (XMLSyntaxError, DocumentInvalid):
            # NB: our schema definitions catch uploads with no parts here
            raise MalformedXML()
        except ErrorResponse:
            raise
        except Exception as e:
            self.logger.error(e)
            raise

        s3_etag = '%s-%d' % (s3_etag_hasher.hexdigest(), len(manifest))
        headers[sysmeta_header('object', 'etag')] = s3_etag
        # Leave base header value blank; SLO will populate
        c_etag = '; s3_etag=%s' % s3_etag
        headers[get_container_update_override_key('etag')] = c_etag

        too_small_message = ('s3api requires that each segment be at least '
                             '%d bytes' % self.conf.min_segment_size)

        def size_checker(manifest):
            # Check the size of each segment except the last and make sure
            # they are all more than the minimum upload chunk size.
            # Note that we need to use the *internal* keys, since we're
            # looking at the manifest that's about to be written.
            return [(item['name'], too_small_message) for item in manifest[:-1]
                    if item and item['bytes'] < self.conf.min_segment_size]

        req.environ['swift.callback.slo_manifest_hook'] = size_checker
        start_time = time.time()

        def response_iter():
            # NB: XML requires that the XML declaration, if present, be at the
            # very start of the document. Clients *will* call us out on not
            # being valid XML if we pass through whitespace before it.
            # Track whether we've sent anything yet so we can yield out that
            # declaration *first*
            yielded_anything = False

            try:
                try:
                    # TODO: add support for versioning
                    put_resp = req.get_response(self.app,
                                                'PUT',
                                                body=json.dumps(manifest),
                                                query={
                                                    'multipart-manifest':
                                                    'put',
                                                    'heartbeat': 'on'
                                                },
                                                headers=headers)
                    if put_resp.status_int == 202:
                        body = []
                        put_resp.fix_conditional_response()
                        for chunk in put_resp.response_iter:
                            if not chunk.strip():
                                if time.time() - start_time < 10:
                                    # Include some grace period to keep
                                    # ceph-s3tests happy
                                    continue
                                if not yielded_anything:
                                    yield (b'<?xml version="1.0" '
                                           b'encoding="UTF-8"?>\n')
                                yielded_anything = True
                                yield chunk
                                continue
                            body.append(chunk)
                        body = json.loads(b''.join(body))
                        if body['Response Status'] != '201 Created':
                            for seg, err in body['Errors']:
                                if err == too_small_message:
                                    raise EntityTooSmall()
                                elif err in ('Etag Mismatch', '404 Not Found'):
                                    raise InvalidPart(upload_id=upload_id)
                            raise InvalidRequest(
                                status=body['Response Status'],
                                msg='\n'.join(': '.join(err)
                                              for err in body['Errors']))
                except BadSwiftRequest as e:
                    msg = str(e)
                    if too_small_message in msg:
                        raise EntityTooSmall(msg)
                    elif ', Etag Mismatch' in msg:
                        raise InvalidPart(upload_id=upload_id)
                    elif ', 404 Not Found' in msg:
                        raise InvalidPart(upload_id=upload_id)
                    else:
                        raise

                # clean up the multipart-upload record
                obj = '%s/%s' % (req.object_name, upload_id)
                try:
                    req.get_response(self.app, 'DELETE', container, obj)
                except NoSuchKey:
                    # We know that this existed long enough for us to HEAD
                    pass

                result_elem = Element('CompleteMultipartUploadResult')

                # NOTE: boto with sig v4 appends port to HTTP_HOST value at
                # the request header when the port is non default value and it
                # makes req.host_url like as http://localhost:8080:8080/path
                # that obviously invalid. Probably it should be resolved at
                # swift.common.swob though, tentatively we are parsing and
                # reconstructing the correct host_url info here.
                # in detail, https://github.com/boto/boto/pull/3513
                parsed_url = urlparse(req.host_url)
                host_url = '%s://%s' % (parsed_url.scheme, parsed_url.hostname)
                # Why are we doing our own port parsing? Because py3 decided
                # to start raising ValueErrors on access after parsing such
                # an invalid port
                netloc = parsed_url.netloc.split('@')[-1].split(']')[-1]
                if ':' in netloc:
                    port = netloc.split(':', 2)[1]
                    host_url += ':%s' % port

                SubElement(result_elem, 'Location').text = host_url + req.path
                SubElement(result_elem, 'Bucket').text = req.container_name
                SubElement(result_elem, 'Key').text = req.object_name
                SubElement(result_elem, 'ETag').text = '"%s"' % s3_etag
                resp.headers.pop('ETag', None)
                if yielded_anything:
                    yield b'\n'
                yield tostring(result_elem,
                               xml_declaration=not yielded_anything)
            except ErrorResponse as err_resp:
                if yielded_anything:
                    err_resp.xml_declaration = False
                    yield b'\n'
                else:
                    # Oh good, we can still change HTTP status code, too!
                    resp.status = err_resp.status
                for chunk in err_resp({}, lambda *a: None):
                    yield chunk

        resp = HTTPOk()  # assume we're good for now... but see above!
        resp.app_iter = reiterate(response_iter())
        resp.content_type = "application/xml"

        return resp
예제 #9
0
    def install_footers_callback(self, req):
        # the proxy controller will call back for footer metadata after
        # body has been sent
        inner_callback = req.environ.get('swift.callback.update_footers')
        # remove any Etag from headers, it won't be valid for ciphertext and
        # we'll send the ciphertext Etag later in footer metadata
        client_etag = req.headers.pop('etag', None)
        override_header = get_container_update_override_key('etag')
        container_listing_etag_header = req.headers.get(override_header)

        def footers_callback(footers):
            if inner_callback:
                # pass on footers dict to any other callback that was
                # registered before this one. It may override any footers that
                # were set.
                inner_callback(footers)

            plaintext_etag = None
            if self.body_crypto_ctxt:
                plaintext_etag = self.plaintext_md5.hexdigest()
                # If client (or other middleware) supplied etag, then validate
                # against plaintext etag
                etag_to_check = footers.get('Etag') or client_etag
                if (etag_to_check is not None
                        and plaintext_etag != etag_to_check):
                    raise HTTPUnprocessableEntity(request=Request(self.env))

                # override any previous notion of etag with the ciphertext etag
                footers['Etag'] = self.ciphertext_md5.hexdigest()

                # Encrypt the plaintext etag using the object key and persist
                # as sysmeta along with the crypto parameters that were used.
                encrypted_etag, etag_crypto_meta = encrypt_header_val(
                    self.crypto, plaintext_etag, self.keys['object'])
                footers['X-Object-Sysmeta-Crypto-Etag'] = \
                    append_crypto_meta(encrypted_etag, etag_crypto_meta)
                footers['X-Object-Sysmeta-Crypto-Body-Meta'] = \
                    dump_crypto_meta(self.body_crypto_meta)

                # Also add an HMAC of the etag for use when evaluating
                # conditional requests
                footers['X-Object-Sysmeta-Crypto-Etag-Mac'] = _hmac_etag(
                    self.keys['object'], plaintext_etag)
            else:
                # No data was read from body, nothing was encrypted, so don't
                # set any crypto sysmeta for the body, but do re-instate any
                # etag provided in inbound request if other middleware has not
                # already set a value.
                if client_etag is not None:
                    footers.setdefault('Etag', client_etag)

            # When deciding on the etag that should appear in container
            # listings, look for:
            #   * override in the footer, otherwise
            #   * override in the header, and finally
            #   * MD5 of the plaintext received
            # This may be None if no override was set and no data was read. An
            # override value of '' will be passed on.
            container_listing_etag = footers.get(
                override_header, container_listing_etag_header)

            if container_listing_etag is None:
                container_listing_etag = plaintext_etag

            if (container_listing_etag
                    and (container_listing_etag != MD5_OF_EMPTY_STRING
                         or plaintext_etag)):
                # Encrypt the container-listing etag using the container key
                # and a random IV, and use it to override the container update
                # value, with the crypto parameters appended. We use the
                # container key here so that only that key is required to
                # decrypt all etag values in a container listing when handling
                # a container GET request. Don't encrypt an EMPTY_ETAG
                # unless there actually was some body content, in which case
                # the container-listing etag is possibly conveying some
                # non-obvious information.
                val, crypto_meta = encrypt_header_val(self.crypto,
                                                      container_listing_etag,
                                                      self.keys['container'])
                crypto_meta['key_id'] = self.keys['id']
                footers[override_header] = \
                    append_crypto_meta(val, crypto_meta)
            # else: no override was set and no data was read

        req.environ['swift.callback.update_footers'] = footers_callback