swift 2.33.0__py2.py3-none-any.whl → 2.34.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- swift/account/auditor.py +11 -0
- swift/account/reaper.py +11 -1
- swift/account/replicator.py +22 -0
- swift/account/server.py +12 -1
- swift-2.33.0.data/scripts/swift-account-audit → swift/cli/account_audit.py +6 -2
- swift-2.33.0.data/scripts/swift-config → swift/cli/config.py +1 -1
- swift-2.33.0.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +6 -2
- swift-2.33.0.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +12 -3
- swift-2.33.0.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +6 -2
- swift/cli/info.py +103 -2
- swift-2.33.0.data/scripts/swift-oldies → swift/cli/oldies.py +6 -3
- swift-2.33.0.data/scripts/swift-orphans → swift/cli/orphans.py +7 -2
- swift/cli/recon_cron.py +5 -5
- swift-2.33.0.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
- swift/cli/relinker.py +1 -1
- swift/cli/ringbuilder.py +24 -0
- swift/common/db.py +2 -1
- swift/common/db_auditor.py +2 -2
- swift/common/db_replicator.py +6 -0
- swift/common/exceptions.py +12 -0
- swift/common/manager.py +102 -0
- swift/common/memcached.py +6 -13
- swift/common/middleware/account_quotas.py +144 -43
- swift/common/middleware/backend_ratelimit.py +166 -24
- swift/common/middleware/catch_errors.py +1 -3
- swift/common/middleware/cname_lookup.py +3 -5
- swift/common/middleware/container_sync.py +6 -10
- swift/common/middleware/crypto/crypto_utils.py +4 -5
- swift/common/middleware/crypto/decrypter.py +4 -5
- swift/common/middleware/crypto/kms_keymaster.py +2 -1
- swift/common/middleware/proxy_logging.py +22 -16
- swift/common/middleware/ratelimit.py +6 -7
- swift/common/middleware/recon.py +6 -7
- swift/common/middleware/s3api/acl_handlers.py +9 -0
- swift/common/middleware/s3api/controllers/multi_upload.py +1 -9
- swift/common/middleware/s3api/controllers/obj.py +20 -1
- swift/common/middleware/s3api/s3api.py +2 -0
- swift/common/middleware/s3api/s3request.py +171 -62
- swift/common/middleware/s3api/s3response.py +35 -6
- swift/common/middleware/s3api/s3token.py +2 -2
- swift/common/middleware/s3api/utils.py +1 -0
- swift/common/middleware/slo.py +153 -52
- swift/common/middleware/tempauth.py +6 -4
- swift/common/middleware/tempurl.py +2 -2
- swift/common/middleware/x_profile/exceptions.py +1 -4
- swift/common/middleware/x_profile/html_viewer.py +9 -10
- swift/common/middleware/x_profile/profile_model.py +1 -2
- swift/common/middleware/xprofile.py +1 -2
- swift/common/request_helpers.py +69 -0
- swift/common/statsd_client.py +207 -0
- swift/common/utils/__init__.py +97 -1635
- swift/common/utils/base.py +138 -0
- swift/common/utils/config.py +443 -0
- swift/common/utils/logs.py +999 -0
- swift/common/wsgi.py +11 -3
- swift/container/auditor.py +11 -0
- swift/container/backend.py +10 -10
- swift/container/reconciler.py +11 -2
- swift/container/replicator.py +22 -1
- swift/container/server.py +12 -1
- swift/container/sharder.py +36 -12
- swift/container/sync.py +11 -1
- swift/container/updater.py +11 -2
- swift/obj/auditor.py +18 -2
- swift/obj/diskfile.py +8 -6
- swift/obj/expirer.py +155 -36
- swift/obj/reconstructor.py +28 -4
- swift/obj/replicator.py +61 -22
- swift/obj/server.py +64 -36
- swift/obj/updater.py +11 -2
- swift/proxy/controllers/base.py +38 -22
- swift/proxy/controllers/obj.py +23 -26
- swift/proxy/server.py +15 -1
- {swift-2.33.0.dist-info → swift-2.34.0.dist-info}/AUTHORS +11 -3
- {swift-2.33.0.dist-info → swift-2.34.0.dist-info}/METADATA +6 -5
- {swift-2.33.0.dist-info → swift-2.34.0.dist-info}/RECORD +81 -107
- {swift-2.33.0.dist-info → swift-2.34.0.dist-info}/entry_points.txt +38 -0
- swift-2.34.0.dist-info/pbr.json +1 -0
- swift-2.33.0.data/scripts/swift-account-auditor +0 -23
- swift-2.33.0.data/scripts/swift-account-info +0 -52
- swift-2.33.0.data/scripts/swift-account-reaper +0 -23
- swift-2.33.0.data/scripts/swift-account-replicator +0 -34
- swift-2.33.0.data/scripts/swift-account-server +0 -23
- swift-2.33.0.data/scripts/swift-container-auditor +0 -23
- swift-2.33.0.data/scripts/swift-container-info +0 -59
- swift-2.33.0.data/scripts/swift-container-reconciler +0 -21
- swift-2.33.0.data/scripts/swift-container-replicator +0 -34
- swift-2.33.0.data/scripts/swift-container-server +0 -23
- swift-2.33.0.data/scripts/swift-container-sharder +0 -37
- swift-2.33.0.data/scripts/swift-container-sync +0 -23
- swift-2.33.0.data/scripts/swift-container-updater +0 -23
- swift-2.33.0.data/scripts/swift-dispersion-report +0 -24
- swift-2.33.0.data/scripts/swift-form-signature +0 -20
- swift-2.33.0.data/scripts/swift-init +0 -119
- swift-2.33.0.data/scripts/swift-object-auditor +0 -29
- swift-2.33.0.data/scripts/swift-object-expirer +0 -33
- swift-2.33.0.data/scripts/swift-object-info +0 -60
- swift-2.33.0.data/scripts/swift-object-reconstructor +0 -33
- swift-2.33.0.data/scripts/swift-object-relinker +0 -23
- swift-2.33.0.data/scripts/swift-object-replicator +0 -37
- swift-2.33.0.data/scripts/swift-object-server +0 -27
- swift-2.33.0.data/scripts/swift-object-updater +0 -23
- swift-2.33.0.data/scripts/swift-proxy-server +0 -23
- swift-2.33.0.data/scripts/swift-recon +0 -24
- swift-2.33.0.data/scripts/swift-recon-cron +0 -24
- swift-2.33.0.data/scripts/swift-ring-builder +0 -37
- swift-2.33.0.data/scripts/swift-ring-builder-analyzer +0 -22
- swift-2.33.0.data/scripts/swift-ring-composer +0 -22
- swift-2.33.0.dist-info/pbr.json +0 -1
- {swift-2.33.0.dist-info → swift-2.34.0.dist-info}/LICENSE +0 -0
- {swift-2.33.0.dist-info → swift-2.34.0.dist-info}/WHEEL +0 -0
- {swift-2.33.0.dist-info → swift-2.34.0.dist-info}/top_level.txt +0 -0
@@ -56,7 +56,8 @@ from swift.common.middleware.s3api.s3response import AccessDenied, \
|
|
56
56
|
MissingContentLength, InvalidStorageClass, S3NotImplemented, InvalidURI, \
|
57
57
|
MalformedXML, InvalidRequest, RequestTimeout, InvalidBucketName, \
|
58
58
|
BadDigest, AuthorizationHeaderMalformed, SlowDown, \
|
59
|
-
AuthorizationQueryParametersError, ServiceUnavailable, BrokenMPU
|
59
|
+
AuthorizationQueryParametersError, ServiceUnavailable, BrokenMPU, \
|
60
|
+
InvalidPartNumber, InvalidPartArgument, XAmzContentSHA256Mismatch
|
60
61
|
from swift.common.middleware.s3api.exception import NotS3Request
|
61
62
|
from swift.common.middleware.s3api.utils import utf8encode, \
|
62
63
|
S3Timestamp, mktime, MULTIUPLOAD_SUFFIX
|
@@ -119,6 +120,20 @@ def _header_acl_property(resource):
|
|
119
120
|
doc='Get and set the %s acl property' % resource)
|
120
121
|
|
121
122
|
|
123
|
+
class S3InputSHA256Mismatch(BaseException):
|
124
|
+
"""
|
125
|
+
Client provided a X-Amz-Content-SHA256, but it doesn't match the data.
|
126
|
+
|
127
|
+
Inherit from BaseException (rather than Exception) so it cuts from the
|
128
|
+
proxy-server app (which will presumably be the one reading the input)
|
129
|
+
through all the layers of the pipeline back to us. It should never escape
|
130
|
+
the s3api middleware.
|
131
|
+
"""
|
132
|
+
def __init__(self, expected, computed):
|
133
|
+
self.expected = expected
|
134
|
+
self.computed = computed
|
135
|
+
|
136
|
+
|
122
137
|
class HashingInput(object):
|
123
138
|
"""
|
124
139
|
wsgi.input wrapper to verify the hash of the input as it's read.
|
@@ -129,6 +144,13 @@ class HashingInput(object):
|
|
129
144
|
self._to_read = content_length
|
130
145
|
self._hasher = hasher()
|
131
146
|
self._expected = expected_hex_hash
|
147
|
+
if content_length == 0 and \
|
148
|
+
self._hasher.hexdigest() != self._expected.lower():
|
149
|
+
self.close()
|
150
|
+
raise XAmzContentSHA256Mismatch(
|
151
|
+
client_computed_content_s_h_a256=self._expected,
|
152
|
+
s3_computed_content_s_h_a256=self._hasher.hexdigest(),
|
153
|
+
)
|
132
154
|
|
133
155
|
def read(self, size=None):
|
134
156
|
chunk = self._input.read(size)
|
@@ -137,12 +159,12 @@ class HashingInput(object):
|
|
137
159
|
short_read = bool(chunk) if size is None else (len(chunk) < size)
|
138
160
|
if self._to_read < 0 or (short_read and self._to_read) or (
|
139
161
|
self._to_read == 0 and
|
140
|
-
self._hasher.hexdigest() != self._expected):
|
162
|
+
self._hasher.hexdigest() != self._expected.lower()):
|
141
163
|
self.close()
|
142
164
|
# Since we don't return the last chunk, the PUT never completes
|
143
|
-
raise
|
144
|
-
|
145
|
-
|
165
|
+
raise S3InputSHA256Mismatch(
|
166
|
+
self._expected,
|
167
|
+
self._hasher.hexdigest())
|
146
168
|
return chunk
|
147
169
|
|
148
170
|
def close(self):
|
@@ -237,6 +259,28 @@ class SigV4Mixin(object):
|
|
237
259
|
if int(self.timestamp) + expires < S3Timestamp.now():
|
238
260
|
raise AccessDenied('Request has expired', reason='expired')
|
239
261
|
|
262
|
+
def _validate_sha256(self):
|
263
|
+
aws_sha256 = self.headers.get('x-amz-content-sha256')
|
264
|
+
looks_like_sha256 = (
|
265
|
+
aws_sha256 and len(aws_sha256) == 64 and
|
266
|
+
all(c in '0123456789abcdef' for c in aws_sha256.lower()))
|
267
|
+
if not aws_sha256:
|
268
|
+
if 'X-Amz-Credential' in self.params:
|
269
|
+
pass # pre-signed URL; not required
|
270
|
+
else:
|
271
|
+
msg = 'Missing required header for this request: ' \
|
272
|
+
'x-amz-content-sha256'
|
273
|
+
raise InvalidRequest(msg)
|
274
|
+
elif aws_sha256 == 'UNSIGNED-PAYLOAD':
|
275
|
+
pass
|
276
|
+
elif not looks_like_sha256 and 'X-Amz-Credential' not in self.params:
|
277
|
+
raise InvalidArgument(
|
278
|
+
'x-amz-content-sha256',
|
279
|
+
aws_sha256,
|
280
|
+
'x-amz-content-sha256 must be UNSIGNED-PAYLOAD, or '
|
281
|
+
'a valid sha256 value.')
|
282
|
+
return aws_sha256
|
283
|
+
|
240
284
|
def _parse_credential(self, credential_string):
|
241
285
|
parts = credential_string.split("/")
|
242
286
|
# credential must be in following format:
|
@@ -447,30 +491,9 @@ class SigV4Mixin(object):
|
|
447
491
|
cr.append(b';'.join(swob.wsgi_to_bytes(k) for k, v in headers_to_sign))
|
448
492
|
|
449
493
|
# 6. Add payload string at the tail
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
elif 'X-Amz-Content-SHA256' not in self.headers:
|
454
|
-
msg = 'Missing required header for this request: ' \
|
455
|
-
'x-amz-content-sha256'
|
456
|
-
raise InvalidRequest(msg)
|
457
|
-
else:
|
458
|
-
hashed_payload = self.headers['X-Amz-Content-SHA256']
|
459
|
-
if hashed_payload != 'UNSIGNED-PAYLOAD':
|
460
|
-
if self.content_length == 0:
|
461
|
-
if hashed_payload.lower() != sha256().hexdigest():
|
462
|
-
raise BadDigest(
|
463
|
-
'The X-Amz-Content-SHA56 you specified did not '
|
464
|
-
'match what we received.')
|
465
|
-
elif self.content_length:
|
466
|
-
self.environ['wsgi.input'] = HashingInput(
|
467
|
-
self.environ['wsgi.input'],
|
468
|
-
self.content_length,
|
469
|
-
sha256,
|
470
|
-
hashed_payload.lower())
|
471
|
-
# else, length not provided -- Swift will kick out a
|
472
|
-
# 411 Length Required which will get translated back
|
473
|
-
# to a S3-style response in S3Request._swift_error_codes
|
494
|
+
hashed_payload = self.headers.get('X-Amz-Content-SHA256',
|
495
|
+
'UNSIGNED-PAYLOAD')
|
496
|
+
|
474
497
|
cr.append(swob.wsgi_to_bytes(hashed_payload))
|
475
498
|
return b'\n'.join(cr)
|
476
499
|
|
@@ -558,6 +581,57 @@ class S3Request(swob.Request):
|
|
558
581
|
# by full URL when absolute path given. See swift.swob for more detail.
|
559
582
|
self.environ['swift.leave_relative_location'] = True
|
560
583
|
|
584
|
+
def validate_part_number(self, parts_count=None, check_max=True):
|
585
|
+
"""
|
586
|
+
Get the partNumber param, if it exists, and check it is valid.
|
587
|
+
|
588
|
+
To be valid, a partNumber must satisfy two criteria. First, it must be
|
589
|
+
an integer between 1 and the maximum allowed parts, inclusive. The
|
590
|
+
maximum allowed parts is the maximum of the configured
|
591
|
+
``max_upload_part_num`` and, if given, ``parts_count``. Second, the
|
592
|
+
partNumber must be less than or equal to the ``parts_count``, if it is
|
593
|
+
given.
|
594
|
+
|
595
|
+
:param parts_count: if given, this is the number of parts in an
|
596
|
+
existing object.
|
597
|
+
:raises InvalidPartArgument: if the partNumber param is invalid i.e.
|
598
|
+
less than 1 or greater than the maximum allowed parts.
|
599
|
+
:raises InvalidPartNumber: if the partNumber param is valid but greater
|
600
|
+
than ``num_parts``.
|
601
|
+
:return: an integer part number if the partNumber param exists,
|
602
|
+
otherwise ``None``.
|
603
|
+
"""
|
604
|
+
part_number = self.params.get('partNumber')
|
605
|
+
if part_number is None:
|
606
|
+
return None
|
607
|
+
|
608
|
+
if self.range:
|
609
|
+
raise InvalidRequest('Cannot specify both Range header and '
|
610
|
+
'partNumber query parameter')
|
611
|
+
|
612
|
+
try:
|
613
|
+
parts_count = int(parts_count)
|
614
|
+
except (TypeError, ValueError):
|
615
|
+
# an invalid/empty param is treated like parts_count=max_parts
|
616
|
+
parts_count = self.conf.max_upload_part_num
|
617
|
+
# max_parts may be raised to the number of existing parts
|
618
|
+
max_parts = max(self.conf.max_upload_part_num, parts_count)
|
619
|
+
|
620
|
+
try:
|
621
|
+
part_number = int(part_number)
|
622
|
+
if part_number < 1:
|
623
|
+
raise ValueError
|
624
|
+
except ValueError:
|
625
|
+
raise InvalidPartArgument(max_parts, part_number) # 400
|
626
|
+
|
627
|
+
if check_max:
|
628
|
+
if part_number > max_parts:
|
629
|
+
raise InvalidPartArgument(max_parts, part_number) # 400
|
630
|
+
if part_number > parts_count:
|
631
|
+
raise InvalidPartNumber() # 416
|
632
|
+
|
633
|
+
return part_number
|
634
|
+
|
561
635
|
def check_signature(self, secret):
|
562
636
|
secret = utf8encode(secret)
|
563
637
|
user_signature = self.signature
|
@@ -747,6 +821,9 @@ class S3Request(swob.Request):
|
|
747
821
|
if delta > self.conf.allowable_clock_skew:
|
748
822
|
raise RequestTimeTooSkewed()
|
749
823
|
|
824
|
+
def _validate_sha256(self):
|
825
|
+
return self.headers.get('x-amz-content-sha256')
|
826
|
+
|
750
827
|
def _validate_headers(self):
|
751
828
|
if 'CONTENT_LENGTH' in self.environ:
|
752
829
|
try:
|
@@ -757,21 +834,6 @@ class S3Request(swob.Request):
|
|
757
834
|
raise InvalidArgument('Content-Length',
|
758
835
|
self.environ['CONTENT_LENGTH'])
|
759
836
|
|
760
|
-
value = _header_strip(self.headers.get('Content-MD5'))
|
761
|
-
if value is not None:
|
762
|
-
if not re.match('^[A-Za-z0-9+/]+={0,2}$', value):
|
763
|
-
# Non-base64-alphabet characters in value.
|
764
|
-
raise InvalidDigest(content_md5=value)
|
765
|
-
try:
|
766
|
-
self.headers['ETag'] = binascii.b2a_hex(
|
767
|
-
binascii.a2b_base64(value))
|
768
|
-
except binascii.Error:
|
769
|
-
# incorrect padding, most likely
|
770
|
-
raise InvalidDigest(content_md5=value)
|
771
|
-
|
772
|
-
if len(self.headers['ETag']) != 32:
|
773
|
-
raise InvalidDigest(content_md5=value)
|
774
|
-
|
775
837
|
if self.method == 'PUT' and any(h in self.headers for h in (
|
776
838
|
'If-Match', 'If-None-Match',
|
777
839
|
'If-Modified-Since', 'If-Unmodified-Since')):
|
@@ -817,6 +879,38 @@ class S3Request(swob.Request):
|
|
817
879
|
if 'x-amz-website-redirect-location' in self.headers:
|
818
880
|
raise S3NotImplemented('Website redirection is not supported.')
|
819
881
|
|
882
|
+
aws_sha256 = self._validate_sha256()
|
883
|
+
if (aws_sha256
|
884
|
+
and aws_sha256 != 'UNSIGNED-PAYLOAD'
|
885
|
+
and self.content_length is not None):
|
886
|
+
# Even if client-provided SHA doesn't look like a SHA, wrap the
|
887
|
+
# input anyway so we'll send the SHA of what the client sent in
|
888
|
+
# the eventual error
|
889
|
+
self.environ['wsgi.input'] = HashingInput(
|
890
|
+
self.environ['wsgi.input'],
|
891
|
+
self.content_length,
|
892
|
+
sha256,
|
893
|
+
aws_sha256)
|
894
|
+
# If no content-length, either client's trying to do a HTTP chunked
|
895
|
+
# transfer, or a HTTP/1.0-style transfer (in which case swift will
|
896
|
+
# reject with length-required and we'll translate back to
|
897
|
+
# MissingContentLength)
|
898
|
+
|
899
|
+
value = _header_strip(self.headers.get('Content-MD5'))
|
900
|
+
if value is not None:
|
901
|
+
if not re.match('^[A-Za-z0-9+/]+={0,2}$', value):
|
902
|
+
# Non-base64-alphabet characters in value.
|
903
|
+
raise InvalidDigest(content_md5=value)
|
904
|
+
try:
|
905
|
+
self.headers['ETag'] = binascii.b2a_hex(
|
906
|
+
binascii.a2b_base64(value))
|
907
|
+
except binascii.Error:
|
908
|
+
# incorrect padding, most likely
|
909
|
+
raise InvalidDigest(content_md5=value)
|
910
|
+
|
911
|
+
if len(self.headers['ETag']) != 32:
|
912
|
+
raise InvalidDigest(content_md5=value)
|
913
|
+
|
820
914
|
# https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
|
821
915
|
# describes some of what would be required to support this
|
822
916
|
if any(['aws-chunked' in self.headers.get('content-encoding', ''),
|
@@ -858,13 +952,11 @@ class S3Request(swob.Request):
|
|
858
952
|
# Limit the read similar to how SLO handles manifests
|
859
953
|
try:
|
860
954
|
body = self.body_file.read(max_length)
|
861
|
-
except
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
'match what we received.')
|
867
|
-
raise
|
955
|
+
except S3InputSHA256Mismatch as err:
|
956
|
+
raise XAmzContentSHA256Mismatch(
|
957
|
+
client_computed_content_s_h_a256=err.expected,
|
958
|
+
s3_computed_content_s_h_a256=err.computed,
|
959
|
+
)
|
868
960
|
else:
|
869
961
|
# No (or zero) Content-Length provided, and not chunked transfer;
|
870
962
|
# no body. Assume zero-length, and enforce a required body below.
|
@@ -1044,7 +1136,10 @@ class S3Request(swob.Request):
|
|
1044
1136
|
if 'logging' in self.params:
|
1045
1137
|
return LoggingStatusController
|
1046
1138
|
if 'partNumber' in self.params:
|
1047
|
-
|
1139
|
+
if self.method == 'PUT':
|
1140
|
+
return PartController
|
1141
|
+
else:
|
1142
|
+
return ObjectController
|
1048
1143
|
if 'uploadId' in self.params:
|
1049
1144
|
return UploadController
|
1050
1145
|
if 'uploads' in self.params:
|
@@ -1087,7 +1182,7 @@ class S3Request(swob.Request):
|
|
1087
1182
|
Create a Swift request based on this request's environment.
|
1088
1183
|
"""
|
1089
1184
|
if self.account is None:
|
1090
|
-
account = self.access_key
|
1185
|
+
account = swob.str_to_wsgi(self.access_key)
|
1091
1186
|
else:
|
1092
1187
|
account = self.account
|
1093
1188
|
|
@@ -1307,6 +1402,16 @@ class S3Request(swob.Request):
|
|
1307
1402
|
return NoSuchKey(obj)
|
1308
1403
|
return NoSuchBucket(container)
|
1309
1404
|
|
1405
|
+
# Since BadDigest ought to plumb in some client-provided values,
|
1406
|
+
# defer evaluation until we know they're provided
|
1407
|
+
def bad_digest_handler():
|
1408
|
+
etag = binascii.hexlify(base64.b64decode(
|
1409
|
+
env['HTTP_CONTENT_MD5']))
|
1410
|
+
return BadDigest(
|
1411
|
+
expected_digest=etag, # yes, really hex
|
1412
|
+
# TODO: plumb in calculated_digest, as b64
|
1413
|
+
)
|
1414
|
+
|
1310
1415
|
code_map = {
|
1311
1416
|
'HEAD': {
|
1312
1417
|
HTTP_NOT_FOUND: not_found_handler,
|
@@ -1315,11 +1420,10 @@ class S3Request(swob.Request):
|
|
1315
1420
|
'GET': {
|
1316
1421
|
HTTP_NOT_FOUND: not_found_handler,
|
1317
1422
|
HTTP_PRECONDITION_FAILED: PreconditionFailed,
|
1318
|
-
HTTP_REQUESTED_RANGE_NOT_SATISFIABLE: InvalidRange,
|
1319
1423
|
},
|
1320
1424
|
'PUT': {
|
1321
1425
|
HTTP_NOT_FOUND: (NoSuchBucket, container),
|
1322
|
-
HTTP_UNPROCESSABLE_ENTITY:
|
1426
|
+
HTTP_UNPROCESSABLE_ENTITY: bad_digest_handler,
|
1323
1427
|
HTTP_REQUEST_ENTITY_TOO_LARGE: EntityTooLarge,
|
1324
1428
|
HTTP_LENGTH_REQUIRED: MissingContentLength,
|
1325
1429
|
HTTP_REQUEST_TIMEOUT: RequestTimeout,
|
@@ -1356,13 +1460,14 @@ class S3Request(swob.Request):
|
|
1356
1460
|
|
1357
1461
|
try:
|
1358
1462
|
sw_resp = sw_req.get_response(app)
|
1359
|
-
except
|
1360
|
-
#
|
1361
|
-
#
|
1362
|
-
# path (e.g. tenant to account translation) will have been made by
|
1363
|
-
# auth middleware
|
1463
|
+
except S3InputSHA256Mismatch as err:
|
1464
|
+
# hopefully by now any modifications to the path (e.g. tenant to
|
1465
|
+
# account translation) will have been made by auth middleware
|
1364
1466
|
self.environ['s3api.backend_path'] = sw_req.environ['PATH_INFO']
|
1365
|
-
|
1467
|
+
raise XAmzContentSHA256Mismatch(
|
1468
|
+
client_computed_content_s_h_a256=err.expected,
|
1469
|
+
s3_computed_content_s_h_a256=err.computed,
|
1470
|
+
)
|
1366
1471
|
else:
|
1367
1472
|
# reuse account
|
1368
1473
|
_, self.account, _ = split_path(sw_resp.environ['PATH_INFO'],
|
@@ -1414,7 +1519,7 @@ class S3Request(swob.Request):
|
|
1414
1519
|
raise InvalidArgument('X-Delete-At',
|
1415
1520
|
self.headers['X-Delete-At'],
|
1416
1521
|
err_str)
|
1417
|
-
if 'X-Delete-After' in
|
1522
|
+
if 'X-Delete-After' in err_str:
|
1418
1523
|
raise InvalidArgument('X-Delete-After',
|
1419
1524
|
self.headers['X-Delete-After'],
|
1420
1525
|
err_str)
|
@@ -1425,6 +1530,10 @@ class S3Request(swob.Request):
|
|
1425
1530
|
**self.signature_does_not_match_kwargs())
|
1426
1531
|
if status == HTTP_FORBIDDEN:
|
1427
1532
|
raise AccessDenied(reason='forbidden')
|
1533
|
+
if status == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
|
1534
|
+
self.validate_part_number(
|
1535
|
+
parts_count=resp.headers.get('x-amz-mp-parts-count'))
|
1536
|
+
raise InvalidRange()
|
1428
1537
|
if status == HTTP_SERVICE_UNAVAILABLE:
|
1429
1538
|
raise ServiceUnavailable()
|
1430
1539
|
if status in (HTTP_RATE_LIMITED, HTTP_TOO_MANY_REQUESTS):
|
@@ -72,6 +72,8 @@ def translate_swift_to_s3(key, val):
|
|
72
72
|
return key, val
|
73
73
|
elif _key == 'x-object-version-id':
|
74
74
|
return 'x-amz-version-id', val
|
75
|
+
elif _key == 'x-parts-count':
|
76
|
+
return 'x-amz-mp-parts-count', val
|
75
77
|
elif _key == 'x-copied-from-version-id':
|
76
78
|
return 'x-amz-copy-source-version-id', val
|
77
79
|
elif _key == 'x-backend-content-type' and \
|
@@ -250,12 +252,17 @@ class ErrorResponse(S3ResponseBase, swob.HTTPException):
|
|
250
252
|
self.headers = HeaderKeyDict(self.headers)
|
251
253
|
|
252
254
|
@property
|
253
|
-
def
|
254
|
-
|
255
|
+
def summary(self):
|
256
|
+
"""Provide a summary of the error code and reason."""
|
255
257
|
if self.reason:
|
256
|
-
|
257
|
-
|
258
|
-
|
258
|
+
summary = '.'.join([self._code, self.reason])
|
259
|
+
else:
|
260
|
+
summary = self._code
|
261
|
+
return summary.replace(' ', '_')
|
262
|
+
|
263
|
+
@property
|
264
|
+
def metric_name(self):
|
265
|
+
return '.'.join([str(self.status_int), self.summary])
|
259
266
|
|
260
267
|
def _body_iter(self):
|
261
268
|
error_elem = Element('Error')
|
@@ -320,6 +327,12 @@ class BadDigest(ErrorResponse):
|
|
320
327
|
_msg = 'The Content-MD5 you specified did not match what we received.'
|
321
328
|
|
322
329
|
|
330
|
+
class XAmzContentSHA256Mismatch(ErrorResponse):
|
331
|
+
_status = '400 Bad Request'
|
332
|
+
_msg = "The provided 'x-amz-content-sha256' header does not match what " \
|
333
|
+
"was computed."
|
334
|
+
|
335
|
+
|
323
336
|
class BucketAlreadyExists(ErrorResponse):
|
324
337
|
_status = '409 Conflict'
|
325
338
|
_msg = 'The requested bucket name is not available. The bucket ' \
|
@@ -436,7 +449,7 @@ class InvalidBucketState(ErrorResponse):
|
|
436
449
|
|
437
450
|
class InvalidDigest(ErrorResponse):
|
438
451
|
_status = '400 Bad Request'
|
439
|
-
_msg = 'The Content-MD5 you specified was
|
452
|
+
_msg = 'The Content-MD5 you specified was invalid.'
|
440
453
|
|
441
454
|
|
442
455
|
class InvalidLocationConstraint(ErrorResponse):
|
@@ -449,6 +462,17 @@ class InvalidObjectState(ErrorResponse):
|
|
449
462
|
_msg = 'The operation is not valid for the current state of the object.'
|
450
463
|
|
451
464
|
|
465
|
+
class InvalidPartArgument(InvalidArgument):
|
466
|
+
_code = 'InvalidArgument'
|
467
|
+
|
468
|
+
def __init__(self, max_parts, value):
|
469
|
+
err_msg = ('Part number must be an integer between '
|
470
|
+
'1 and %s, inclusive' % max_parts)
|
471
|
+
super(InvalidArgument, self).__init__(err_msg,
|
472
|
+
argument_name='partNumber',
|
473
|
+
argument_value=value)
|
474
|
+
|
475
|
+
|
452
476
|
class InvalidPart(ErrorResponse):
|
453
477
|
_status = '400 Bad Request'
|
454
478
|
_msg = 'One or more of the specified parts could not be found. The part ' \
|
@@ -478,6 +502,11 @@ class InvalidRange(ErrorResponse):
|
|
478
502
|
_msg = 'The requested range cannot be satisfied.'
|
479
503
|
|
480
504
|
|
505
|
+
class InvalidPartNumber(ErrorResponse):
|
506
|
+
_status = '416 Requested Range Not Satisfiable'
|
507
|
+
_msg = 'The requested partnumber is not satisfiable'
|
508
|
+
|
509
|
+
|
481
510
|
class InvalidRequest(ErrorResponse):
|
482
511
|
_status = '400 Bad Request'
|
483
512
|
_msg = 'Invalid Request.'
|
@@ -65,7 +65,7 @@ import six
|
|
65
65
|
from six.moves import urllib
|
66
66
|
|
67
67
|
from swift.common.swob import Request, HTTPBadRequest, HTTPUnauthorized, \
|
68
|
-
HTTPException
|
68
|
+
HTTPException, str_to_wsgi
|
69
69
|
from swift.common.utils import config_true_value, split_path, get_logger, \
|
70
70
|
cache_from_env, append_underscore
|
71
71
|
from swift.common.wsgi import ConfigFileError
|
@@ -404,7 +404,7 @@ class S3Token(object):
|
|
404
404
|
self._logger.debug('Connecting with tenant: %s', tenant_to_connect)
|
405
405
|
new_tenant_name = '%s%s' % (self._reseller_prefix, tenant_to_connect)
|
406
406
|
environ['PATH_INFO'] = environ['PATH_INFO'].replace(
|
407
|
-
account, new_tenant_name, 1)
|
407
|
+
str_to_wsgi(account), str_to_wsgi(new_tenant_name), 1)
|
408
408
|
return self._app(environ, start_response)
|
409
409
|
|
410
410
|
|