# Copyright (c) 2014 OpenStack Foundation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License 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 re try: from collections.abc import MutableMapping except ImportError: from collections import MutableMapping # py2 from functools import partial from swift.common import header_key_dict from swift.common import swob from swift.common.utils import config_true_value from swift.common.request_helpers import is_sys_meta from swift.common.middleware.s3api.utils import snake_to_camel, \ sysmeta_prefix, sysmeta_header from swift.common.middleware.s3api.etree import Element, SubElement, tostring from swift.common.middleware.versioned_writes.object_versioning import \ DELETE_MARKER_CONTENT_TYPE class HeaderKeyDict(header_key_dict.HeaderKeyDict): """ Similar to the Swift's normal HeaderKeyDict class, but its key name is normalized as S3 clients expect. """ @staticmethod def _title(s): s = header_key_dict.HeaderKeyDict._title(s) if s.lower() == 'etag': # AWS Java SDK expects only 'ETag'. return 'ETag' if s.lower().startswith('x-amz-'): # AWS headers returned by S3 are lowercase. return swob.bytes_to_wsgi(swob.wsgi_to_bytes(s).lower()) return s def translate_swift_to_s3(key, val): _key = swob.bytes_to_wsgi(swob.wsgi_to_bytes(key).lower()) def translate_meta_key(_key): if not _key.startswith('x-object-meta-'): return _key # Note that AWS allows user-defined metadata with underscores in the # header, while WSGI (and other protocols derived from CGI) does not # differentiate between an underscore and a dash. Fortunately, # eventlet exposes the raw headers from the client, so we could # translate '_' to '=5F' on the way in. Now, we translate back. return 'x-amz-meta-' + _key[14:].replace('=5f', '_') if _key.startswith('x-object-meta-'): return translate_meta_key(_key), val elif _key in ('content-length', 'content-type', 'content-range', 'content-encoding', 'content-disposition', 'content-language', 'etag', 'last-modified', 'x-robots-tag', 'cache-control', 'expires'): return key, val elif _key == 'x-object-version-id': return 'x-amz-version-id', val elif _key == 'x-copied-from-version-id': return 'x-amz-copy-source-version-id', val elif _key == 'x-backend-content-type' and \ val == DELETE_MARKER_CONTENT_TYPE: return 'x-amz-delete-marker', 'true' elif _key == 'access-control-expose-headers': exposed_headers = val.split(', ') exposed_headers.extend([ 'x-amz-request-id', 'x-amz-id-2', ]) return 'access-control-expose-headers', ', '.join( translate_meta_key(h) for h in exposed_headers) elif _key == 'access-control-allow-methods': methods = val.split(', ') try: methods.remove('COPY') # that's not a thing in S3 except ValueError: pass # not there? don't worry about it return key, ', '.join(methods) elif _key.startswith('access-control-'): return key, val # else, drop the header return None class S3ResponseBase(object): """ Base class for swift3 responses. """ pass class S3Response(S3ResponseBase, swob.Response): """ Similar to the Response class in Swift, but uses our HeaderKeyDict for headers instead of Swift's HeaderKeyDict. This also translates Swift specific headers to S3 headers. """ def __init__(self, *args, **kwargs): swob.Response.__init__(self, *args, **kwargs) s3_sysmeta_headers = swob.HeaderKeyDict() sw_headers = swob.HeaderKeyDict() headers = HeaderKeyDict() self.is_slo = False def is_swift3_sysmeta(sysmeta_key, server_type): swift3_sysmeta_prefix = ( 'x-%s-sysmeta-swift3' % server_type).lower() return sysmeta_key.lower().startswith(swift3_sysmeta_prefix) def is_s3api_sysmeta(sysmeta_key, server_type): s3api_sysmeta_prefix = sysmeta_prefix(_server_type).lower() return sysmeta_key.lower().startswith(s3api_sysmeta_prefix) for key, val in self.headers.items(): if is_sys_meta('object', key) or is_sys_meta('container', key): _server_type = key.split('-')[1] if is_swift3_sysmeta(key, _server_type): # To be compatible with older swift3, translate swift3 # sysmeta to s3api sysmeta here key = sysmeta_prefix(_server_type) + \ key[len('x-%s-sysmeta-swift3-' % _server_type):] if key not in s3_sysmeta_headers: # To avoid overwrite s3api sysmeta by older swift3 # sysmeta set the key only when the key does not exist s3_sysmeta_headers[key] = val elif is_s3api_sysmeta(key, _server_type): s3_sysmeta_headers[key] = val else: sw_headers[key] = val else: sw_headers[key] = val # Handle swift headers for key, val in sw_headers.items(): s3_pair = translate_swift_to_s3(key, val) if s3_pair is None: continue headers[s3_pair[0]] = s3_pair[1] self.is_slo = config_true_value(sw_headers.get( 'x-static-large-object')) # Check whether we stored the AWS-style etag on upload override_etag = s3_sysmeta_headers.get( sysmeta_header('object', 'etag')) if override_etag not in (None, ''): # Multipart uploads in AWS have ETags like # - headers['etag'] = override_etag elif self.is_slo and 'etag' in headers: # Many AWS clients use the presence of a '-' to decide whether # to attempt client-side download validation, so even if we # didn't store the AWS-style header, tack on a '-N'. (Use 'N' # because we don't actually know how many parts there are.) headers['etag'] += '-N' self.headers = headers if self.etag: # add double quotes to the etag header self.etag = self.etag # Used for pure swift header handling at the request layer self.sw_headers = sw_headers self.sysmeta_headers = s3_sysmeta_headers @classmethod def from_swift_resp(cls, sw_resp): """ Create a new S3 response object based on the given Swift response. """ if sw_resp.app_iter: body = None app_iter = sw_resp.app_iter else: body = sw_resp.body app_iter = None resp = cls(status=sw_resp.status, headers=sw_resp.headers, request=sw_resp.request, body=body, app_iter=app_iter, conditional_response=sw_resp.conditional_response) resp.environ.update(sw_resp.environ) return resp def append_copy_resp_body(self, controller_name, last_modified): elem = Element('Copy%sResult' % controller_name) SubElement(elem, 'LastModified').text = last_modified SubElement(elem, 'ETag').text = '"%s"' % self.etag self.headers['Content-Type'] = 'application/xml' self.body = tostring(elem) self.etag = None HTTPOk = partial(S3Response, status=200) HTTPCreated = partial(S3Response, status=201) HTTPAccepted = partial(S3Response, status=202) HTTPNoContent = partial(S3Response, status=204) HTTPPartialContent = partial(S3Response, status=206) class ErrorResponse(S3ResponseBase, swob.HTTPException): """ S3 error object. Reference information about S3 errors is available at: http://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html """ _status = '' _msg = '' _code = '' xml_declaration = True def __init__(self, msg=None, *args, **kwargs): if msg: self._msg = msg if not self._code: self._code = self.__class__.__name__ self.info = kwargs.copy() for reserved_key in ('headers', 'body'): if self.info.get(reserved_key): del(self.info[reserved_key]) swob.HTTPException.__init__( self, status=kwargs.pop('status', self._status), app_iter=self._body_iter(), content_type='application/xml', *args, **kwargs) self.headers = HeaderKeyDict(self.headers) def _body_iter(self): error_elem = Element('Error') SubElement(error_elem, 'Code').text = self._code SubElement(error_elem, 'Message').text = self._msg if 'swift.trans_id' in self.environ: request_id = self.environ['swift.trans_id'] SubElement(error_elem, 'RequestId').text = request_id self._dict_to_etree(error_elem, self.info) yield tostring(error_elem, use_s3ns=False, xml_declaration=self.xml_declaration) def _dict_to_etree(self, parent, d): for key, value in d.items(): tag = re.sub(r'\W', '', snake_to_camel(key)) elem = SubElement(parent, tag) if isinstance(value, (dict, MutableMapping)): self._dict_to_etree(elem, value) else: if isinstance(value, (int, float, bool)): value = str(value) try: elem.text = value except ValueError: # We set an invalid string for XML. elem.text = '(invalid string)' class AccessDenied(ErrorResponse): _status = '403 Forbidden' _msg = 'Access Denied.' class AccountProblem(ErrorResponse): _status = '403 Forbidden' _msg = 'There is a problem with your AWS account that prevents the ' \ 'operation from completing successfully.' class AmbiguousGrantByEmailAddress(ErrorResponse): _status = '400 Bad Request' _msg = 'The e-mail address you provided is associated with more than ' \ 'one account.' class AuthorizationHeaderMalformed(ErrorResponse): _status = '400 Bad Request' _msg = 'The authorization header is malformed; the authorization ' \ 'header requires three components: Credential, SignedHeaders, ' \ 'and Signature.' class AuthorizationQueryParametersError(ErrorResponse): _status = '400 Bad Request' class BadDigest(ErrorResponse): _status = '400 Bad Request' _msg = 'The Content-MD5 you specified did not match what we received.' class BucketAlreadyExists(ErrorResponse): _status = '409 Conflict' _msg = 'The requested bucket name is not available. The bucket ' \ 'namespace is shared by all users of the system. Please select a ' \ 'different name and try again.' def __init__(self, bucket, msg=None, *args, **kwargs): ErrorResponse.__init__(self, msg, bucket_name=bucket, *args, **kwargs) class BucketAlreadyOwnedByYou(ErrorResponse): _status = '409 Conflict' _msg = 'Your previous request to create the named bucket succeeded and ' \ 'you already own it.' def __init__(self, bucket, msg=None, *args, **kwargs): ErrorResponse.__init__(self, msg, bucket_name=bucket, *args, **kwargs) class BucketNotEmpty(ErrorResponse): _status = '409 Conflict' _msg = 'The bucket you tried to delete is not empty' class VersionedBucketNotEmpty(BucketNotEmpty): _msg = 'The bucket you tried to delete is not empty. ' \ 'You must delete all versions in the bucket.' _code = 'BucketNotEmpty' class CredentialsNotSupported(ErrorResponse): _status = '400 Bad Request' _msg = 'This request does not support credentials.' class CrossLocationLoggingProhibited(ErrorResponse): _status = '403 Forbidden' _msg = 'Cross location logging not allowed. Buckets in one geographic ' \ 'location cannot log information to a bucket in another location.' class EntityTooSmall(ErrorResponse): _status = '400 Bad Request' _msg = 'Your proposed upload is smaller than the minimum allowed object ' \ 'size.' class EntityTooLarge(ErrorResponse): _status = '400 Bad Request' _msg = 'Your proposed upload exceeds the maximum allowed object size.' class ExpiredToken(ErrorResponse): _status = '400 Bad Request' _msg = 'The provided token has expired.' class IllegalVersioningConfigurationException(ErrorResponse): _status = '400 Bad Request' _msg = 'The Versioning configuration specified in the request is invalid.' class IncompleteBody(ErrorResponse): _status = '400 Bad Request' _msg = 'You did not provide the number of bytes specified by the ' \ 'Content-Length HTTP header.' class IncorrectNumberOfFilesInPostRequest(ErrorResponse): _status = '400 Bad Request' _msg = 'POST requires exactly one file upload per request.' class InlineDataTooLarge(ErrorResponse): _status = '400 Bad Request' _msg = 'Inline data exceeds the maximum allowed size.' class InternalError(ErrorResponse): _status = '500 Internal Server Error' _msg = 'We encountered an internal error. Please try again.' def __str__(self): return '%s: %s (%s)' % ( self.__class__.__name__, self.status, self._msg) class InvalidAccessKeyId(ErrorResponse): _status = '403 Forbidden' _msg = 'The AWS Access Key Id you provided does not exist in our records.' class InvalidArgument(ErrorResponse): _status = '400 Bad Request' _msg = 'Invalid Argument.' def __init__(self, name, value, msg=None, *args, **kwargs): ErrorResponse.__init__(self, msg, argument_name=name, argument_value=value, *args, **kwargs) class InvalidBucketName(ErrorResponse): _status = '400 Bad Request' _msg = 'The specified bucket is not valid.' def __init__(self, bucket, msg=None, *args, **kwargs): ErrorResponse.__init__(self, msg, bucket_name=bucket, *args, **kwargs) class InvalidBucketState(ErrorResponse): _status = '409 Conflict' _msg = 'The request is not valid with the current state of the bucket.' class InvalidDigest(ErrorResponse): _status = '400 Bad Request' _msg = 'The Content-MD5 you specified was an invalid.' class InvalidLocationConstraint(ErrorResponse): _status = '400 Bad Request' _msg = 'The specified location constraint is not valid.' class InvalidObjectState(ErrorResponse): _status = '403 Forbidden' _msg = 'The operation is not valid for the current state of the object.' class InvalidPart(ErrorResponse): _status = '400 Bad Request' _msg = 'One or more of the specified parts could not be found. The part ' \ 'might not have been uploaded, or the specified entity tag might ' \ 'not have matched the part\'s entity tag.' class InvalidPartOrder(ErrorResponse): _status = '400 Bad Request' _msg = 'The list of parts was not in ascending order.Parts list must ' \ 'specified in order by part number.' class InvalidPayer(ErrorResponse): _status = '403 Forbidden' _msg = 'All access to this object has been disabled.' class InvalidPolicyDocument(ErrorResponse): _status = '400 Bad Request' _msg = 'The content of the form does not meet the conditions specified ' \ 'in the policy document.' class InvalidRange(ErrorResponse): _status = '416 Requested Range Not Satisfiable' _msg = 'The requested range cannot be satisfied.' class InvalidRequest(ErrorResponse): _status = '400 Bad Request' _msg = 'Invalid Request.' class InvalidSecurity(ErrorResponse): _status = '403 Forbidden' _msg = 'The provided security credentials are not valid.' class InvalidSOAPRequest(ErrorResponse): _status = '400 Bad Request' _msg = 'The SOAP request body is invalid.' class InvalidStorageClass(ErrorResponse): _status = '400 Bad Request' _msg = 'The storage class you specified is not valid.' class InvalidTargetBucketForLogging(ErrorResponse): _status = '400 Bad Request' _msg = 'The target bucket for logging does not exist, is not owned by ' \ 'you, or does not have the appropriate grants for the ' \ 'log-delivery group.' def __init__(self, bucket, msg=None, *args, **kwargs): ErrorResponse.__init__(self, msg, target_bucket=bucket, *args, **kwargs) class InvalidToken(ErrorResponse): _status = '400 Bad Request' _msg = 'The provided token is malformed or otherwise invalid.' class InvalidURI(ErrorResponse): _status = '400 Bad Request' _msg = 'Couldn\'t parse the specified URI.' def __init__(self, uri, msg=None, *args, **kwargs): ErrorResponse.__init__(self, msg, uri=uri, *args, **kwargs) class KeyTooLongError(ErrorResponse): _status = '400 Bad Request' _msg = 'Your key is too long.' class MalformedACLError(ErrorResponse): _status = '400 Bad Request' _msg = 'The XML you provided was not well-formed or did not validate ' \ 'against our published schema.' class MalformedPOSTRequest(ErrorResponse): _status = '400 Bad Request' _msg = 'The body of your POST request is not well-formed ' \ 'multipart/form-data.' class MalformedXML(ErrorResponse): _status = '400 Bad Request' _msg = 'The XML you provided was not well-formed or did not validate ' \ 'against our published schema' class MaxMessageLengthExceeded(ErrorResponse): _status = '400 Bad Request' _msg = 'Your request was too big.' class MaxPostPreDataLengthExceededError(ErrorResponse): _status = '400 Bad Request' _msg = 'Your POST request fields preceding the upload file were too large.' class MetadataTooLarge(ErrorResponse): _status = '400 Bad Request' _msg = 'Your metadata headers exceed the maximum allowed metadata size.' class MethodNotAllowed(ErrorResponse): _status = '405 Method Not Allowed' _msg = 'The specified method is not allowed against this resource.' def __init__(self, method, resource_type, msg=None, *args, **kwargs): ErrorResponse.__init__(self, msg, method=method, resource_type=resource_type, *args, **kwargs) class MissingContentLength(ErrorResponse): _status = '411 Length Required' _msg = 'You must provide the Content-Length HTTP header.' class MissingRequestBodyError(ErrorResponse): _status = '400 Bad Request' _msg = 'Request body is empty.' class MissingSecurityElement(ErrorResponse): _status = '400 Bad Request' _msg = 'The SOAP 1.1 request is missing a security element.' class MissingSecurityHeader(ErrorResponse): _status = '400 Bad Request' _msg = 'Your request was missing a required header.' class NoLoggingStatusForKey(ErrorResponse): _status = '400 Bad Request' _msg = 'There is no such thing as a logging status sub-resource for a key.' class NoSuchBucket(ErrorResponse): _status = '404 Not Found' _msg = 'The specified bucket does not exist.' def __init__(self, bucket, msg=None, *args, **kwargs): if not bucket: raise InternalError() ErrorResponse.__init__(self, msg, bucket_name=bucket, *args, **kwargs) class NoSuchKey(ErrorResponse): _status = '404 Not Found' _msg = 'The specified key does not exist.' def __init__(self, key, msg=None, *args, **kwargs): if not key: raise InternalError() ErrorResponse.__init__(self, msg, key=key, *args, **kwargs) class NoSuchLifecycleConfiguration(ErrorResponse): _status = '404 Not Found' _msg = 'The lifecycle configuration does not exist. .' class NoSuchUpload(ErrorResponse): _status = '404 Not Found' _msg = 'The specified multipart upload does not exist. The upload ID ' \ 'might be invalid, or the multipart upload might have been ' \ 'aborted or completed.' class NoSuchVersion(ErrorResponse): _status = '404 Not Found' _msg = 'The specified version does not exist.' def __init__(self, key, version_id, msg=None, *args, **kwargs): if not key: raise InternalError() ErrorResponse.__init__(self, msg, key=key, version_id=version_id, *args, **kwargs) # NotImplemented is a python built-in constant. Use S3NotImplemented instead. class S3NotImplemented(ErrorResponse): _status = '501 Not Implemented' _msg = 'Not implemented.' _code = 'NotImplemented' class NotSignedUp(ErrorResponse): _status = '403 Forbidden' _msg = 'Your account is not signed up for the Amazon S3 service.' class NotSuchBucketPolicy(ErrorResponse): _status = '404 Not Found' _msg = 'The specified bucket does not have a bucket policy.' class OperationAborted(ErrorResponse): _status = '409 Conflict' _msg = 'A conflicting conditional operation is currently in progress ' \ 'against this resource. Please try again.' class PermanentRedirect(ErrorResponse): _status = '301 Moved Permanently' _msg = 'The bucket you are attempting to access must be addressed using ' \ 'the specified endpoint. Please send all future requests to this ' \ 'endpoint.' class PreconditionFailed(ErrorResponse): _status = '412 Precondition Failed' _msg = 'At least one of the preconditions you specified did not hold.' class Redirect(ErrorResponse): _status = '307 Moved Temporarily' _msg = 'Temporary redirect.' class RestoreAlreadyInProgress(ErrorResponse): _status = '409 Conflict' _msg = 'Object restore is already in progress.' class RequestIsNotMultiPartContent(ErrorResponse): _status = '400 Bad Request' _msg = 'Bucket POST must be of the enclosure-type multipart/form-data.' class RequestTimeout(ErrorResponse): _status = '400 Bad Request' _msg = 'Your socket connection to the server was not read from or ' \ 'written to within the timeout period.' class RequestTimeTooSkewed(ErrorResponse): _status = '403 Forbidden' _msg = 'The difference between the request time and the current time ' \ 'is too large.' class RequestTorrentOfBucketError(ErrorResponse): _status = '400 Bad Request' _msg = 'Requesting the torrent file of a bucket is not permitted.' class SignatureDoesNotMatch(ErrorResponse): _status = '403 Forbidden' _msg = 'The request signature we calculated does not match the ' \ 'signature you provided. Check your key and signing method.' class ServiceUnavailable(ErrorResponse): _status = '503 Service Unavailable' _msg = 'Please reduce your request rate.' class SlowDown(ErrorResponse): _status = '503 Slow Down' _msg = 'Please reduce your request rate.' class TemporaryRedirect(ErrorResponse): _status = '307 Moved Temporarily' _msg = 'You are being redirected to the bucket while DNS updates.' class TokenRefreshRequired(ErrorResponse): _status = '400 Bad Request' _msg = 'The provided token must be refreshed.' class TooManyBuckets(ErrorResponse): _status = '400 Bad Request' _msg = 'You have attempted to create more buckets than allowed.' class UnexpectedContent(ErrorResponse): _status = '400 Bad Request' _msg = 'This request does not support content.' class UnresolvableGrantByEmailAddress(ErrorResponse): _status = '400 Bad Request' _msg = 'The e-mail address you provided does not match any account on ' \ 'record.' class UserKeyMustBeSpecified(ErrorResponse): _status = '400 Bad Request' _msg = 'The bucket POST must contain the specified field name. If it is ' \ 'specified, please check the order of the fields.' class BrokenMPU(ErrorResponse): # This is very much a Swift-ism, and we wish we didn't need it _status = '409 Conflict' _msg = 'Multipart upload has broken segment data.'