swift 2.32.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 +13 -12
- swift-2.32.0.data/scripts/swift-account-audit → swift/cli/account_audit.py +6 -2
- swift-2.32.0.data/scripts/swift-config → swift/cli/config.py +1 -1
- swift-2.32.0.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +6 -2
- swift-2.32.0.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +12 -3
- swift-2.32.0.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +6 -2
- swift/cli/info.py +131 -3
- swift-2.32.0.data/scripts/swift-oldies → swift/cli/oldies.py +6 -3
- swift-2.32.0.data/scripts/swift-orphans → swift/cli/orphans.py +7 -2
- swift-2.32.0.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +9 -18
- swift-2.32.0.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
- swift/cli/relinker.py +1 -1
- swift/cli/reload.py +141 -0
- swift/cli/ringbuilder.py +24 -0
- swift/common/daemon.py +12 -2
- swift/common/db.py +14 -9
- swift/common/db_auditor.py +2 -2
- swift/common/db_replicator.py +6 -0
- swift/common/exceptions.py +12 -0
- swift/common/http_protocol.py +76 -3
- swift/common/manager.py +120 -5
- swift/common/memcached.py +24 -25
- 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 +57 -43
- swift/common/middleware/ratelimit.py +6 -7
- swift/common/middleware/recon.py +6 -7
- swift/common/middleware/s3api/acl_handlers.py +10 -1
- swift/common/middleware/s3api/controllers/__init__.py +3 -0
- swift/common/middleware/s3api/controllers/acl.py +3 -2
- swift/common/middleware/s3api/controllers/logging.py +2 -2
- swift/common/middleware/s3api/controllers/multi_upload.py +31 -15
- swift/common/middleware/s3api/controllers/obj.py +20 -1
- swift/common/middleware/s3api/controllers/object_lock.py +44 -0
- swift/common/middleware/s3api/s3api.py +6 -0
- swift/common/middleware/s3api/s3request.py +190 -74
- swift/common/middleware/s3api/s3response.py +48 -8
- swift/common/middleware/s3api/s3token.py +2 -2
- swift/common/middleware/s3api/utils.py +2 -1
- swift/common/middleware/slo.py +508 -310
- swift/common/middleware/staticweb.py +45 -14
- swift/common/middleware/tempauth.py +6 -4
- swift/common/middleware/tempurl.py +134 -93
- 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 +101 -8
- swift/common/statsd_client.py +207 -0
- swift/common/storage_policy.py +1 -1
- swift/common/swob.py +5 -2
- swift/common/utils/__init__.py +331 -1774
- swift/common/utils/base.py +138 -0
- swift/common/utils/config.py +443 -0
- swift/common/utils/logs.py +999 -0
- swift/common/utils/timestamp.py +23 -2
- swift/common/wsgi.py +19 -3
- swift/container/auditor.py +11 -0
- swift/container/backend.py +136 -31
- swift/container/reconciler.py +11 -2
- swift/container/replicator.py +64 -7
- swift/container/server.py +276 -146
- swift/container/sharder.py +86 -42
- swift/container/sync.py +11 -1
- swift/container/updater.py +12 -2
- swift/obj/auditor.py +20 -3
- swift/obj/diskfile.py +63 -25
- swift/obj/expirer.py +154 -47
- swift/obj/mem_diskfile.py +2 -1
- swift/obj/mem_server.py +1 -0
- swift/obj/reconstructor.py +28 -4
- swift/obj/replicator.py +63 -24
- swift/obj/server.py +76 -59
- swift/obj/updater.py +12 -2
- swift/obj/watchers/dark_data.py +72 -34
- swift/proxy/controllers/account.py +3 -2
- swift/proxy/controllers/base.py +254 -148
- swift/proxy/controllers/container.py +274 -289
- swift/proxy/controllers/obj.py +120 -166
- swift/proxy/server.py +17 -13
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/AUTHORS +14 -4
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/METADATA +9 -7
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/RECORD +97 -120
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/entry_points.txt +39 -0
- swift-2.34.0.dist-info/pbr.json +1 -0
- swift-2.32.0.data/scripts/swift-account-auditor +0 -23
- swift-2.32.0.data/scripts/swift-account-info +0 -52
- swift-2.32.0.data/scripts/swift-account-reaper +0 -23
- swift-2.32.0.data/scripts/swift-account-replicator +0 -34
- swift-2.32.0.data/scripts/swift-account-server +0 -23
- swift-2.32.0.data/scripts/swift-container-auditor +0 -23
- swift-2.32.0.data/scripts/swift-container-info +0 -56
- swift-2.32.0.data/scripts/swift-container-reconciler +0 -21
- swift-2.32.0.data/scripts/swift-container-replicator +0 -34
- swift-2.32.0.data/scripts/swift-container-server +0 -23
- swift-2.32.0.data/scripts/swift-container-sharder +0 -37
- swift-2.32.0.data/scripts/swift-container-sync +0 -23
- swift-2.32.0.data/scripts/swift-container-updater +0 -23
- swift-2.32.0.data/scripts/swift-dispersion-report +0 -24
- swift-2.32.0.data/scripts/swift-form-signature +0 -20
- swift-2.32.0.data/scripts/swift-init +0 -119
- swift-2.32.0.data/scripts/swift-object-auditor +0 -29
- swift-2.32.0.data/scripts/swift-object-expirer +0 -33
- swift-2.32.0.data/scripts/swift-object-info +0 -60
- swift-2.32.0.data/scripts/swift-object-reconstructor +0 -33
- swift-2.32.0.data/scripts/swift-object-relinker +0 -23
- swift-2.32.0.data/scripts/swift-object-replicator +0 -37
- swift-2.32.0.data/scripts/swift-object-server +0 -27
- swift-2.32.0.data/scripts/swift-object-updater +0 -23
- swift-2.32.0.data/scripts/swift-proxy-server +0 -23
- swift-2.32.0.data/scripts/swift-recon +0 -24
- swift-2.32.0.data/scripts/swift-ring-builder +0 -37
- swift-2.32.0.data/scripts/swift-ring-builder-analyzer +0 -22
- swift-2.32.0.data/scripts/swift-ring-composer +0 -22
- swift-2.32.0.dist-info/pbr.json +0 -1
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/LICENSE +0 -0
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/WHEEL +0 -0
- {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/top_level.txt +0 -0
@@ -26,7 +26,7 @@ from six.moves.urllib.parse import quote, unquote, parse_qsl
|
|
26
26
|
import string
|
27
27
|
|
28
28
|
from swift.common.utils import split_path, json, close_if_possible, md5, \
|
29
|
-
streq_const_time
|
29
|
+
streq_const_time, get_policy_index
|
30
30
|
from swift.common.registry import get_swift_info
|
31
31
|
from swift.common import swob
|
32
32
|
from swift.common.http import HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, \
|
@@ -47,7 +47,7 @@ from swift.common.middleware.s3api.controllers import ServiceController, \
|
|
47
47
|
LocationController, LoggingStatusController, PartController, \
|
48
48
|
UploadController, UploadsController, VersioningController, \
|
49
49
|
UnsupportedController, S3AclController, BucketController, \
|
50
|
-
TaggingController
|
50
|
+
TaggingController, ObjectLockController
|
51
51
|
from swift.common.middleware.s3api.s3response import AccessDenied, \
|
52
52
|
InvalidArgument, InvalidDigest, BucketAlreadyOwnedByYou, \
|
53
53
|
RequestTimeTooSkewed, S3Response, SignatureDoesNotMatch, \
|
@@ -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
|
@@ -74,7 +75,8 @@ ALLOWED_SUB_RESOURCES = sorted([
|
|
74
75
|
'versionId', 'versioning', 'versions', 'website',
|
75
76
|
'response-cache-control', 'response-content-disposition',
|
76
77
|
'response-content-encoding', 'response-content-language',
|
77
|
-
'response-content-type', 'response-expires', 'cors', 'tagging', 'restore'
|
78
|
+
'response-content-type', 'response-expires', 'cors', 'tagging', 'restore',
|
79
|
+
'object-lock'
|
78
80
|
])
|
79
81
|
|
80
82
|
|
@@ -103,6 +105,7 @@ def _header_acl_property(resource):
|
|
103
105
|
"""
|
104
106
|
Set and retrieve the acl in self.headers
|
105
107
|
"""
|
108
|
+
|
106
109
|
def getter(self):
|
107
110
|
return getattr(self, '_%s' % resource)
|
108
111
|
|
@@ -117,15 +120,37 @@ def _header_acl_property(resource):
|
|
117
120
|
doc='Get and set the %s acl property' % resource)
|
118
121
|
|
119
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
|
+
|
120
137
|
class HashingInput(object):
|
121
138
|
"""
|
122
139
|
wsgi.input wrapper to verify the hash of the input as it's read.
|
123
140
|
"""
|
141
|
+
|
124
142
|
def __init__(self, reader, content_length, hasher, expected_hex_hash):
|
125
143
|
self._input = reader
|
126
144
|
self._to_read = content_length
|
127
145
|
self._hasher = hasher()
|
128
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
|
+
)
|
129
154
|
|
130
155
|
def read(self, size=None):
|
131
156
|
chunk = self._input.read(size)
|
@@ -134,12 +159,12 @@ class HashingInput(object):
|
|
134
159
|
short_read = bool(chunk) if size is None else (len(chunk) < size)
|
135
160
|
if self._to_read < 0 or (short_read and self._to_read) or (
|
136
161
|
self._to_read == 0 and
|
137
|
-
self._hasher.hexdigest() != self._expected):
|
162
|
+
self._hasher.hexdigest() != self._expected.lower()):
|
138
163
|
self.close()
|
139
164
|
# Since we don't return the last chunk, the PUT never completes
|
140
|
-
raise
|
141
|
-
|
142
|
-
|
165
|
+
raise S3InputSHA256Mismatch(
|
166
|
+
self._expected,
|
167
|
+
self._hasher.hexdigest())
|
143
168
|
return chunk
|
144
169
|
|
145
170
|
def close(self):
|
@@ -234,6 +259,28 @@ class SigV4Mixin(object):
|
|
234
259
|
if int(self.timestamp) + expires < S3Timestamp.now():
|
235
260
|
raise AccessDenied('Request has expired', reason='expired')
|
236
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
|
+
|
237
284
|
def _parse_credential(self, credential_string):
|
238
285
|
parts = credential_string.split("/")
|
239
286
|
# credential must be in following format:
|
@@ -444,30 +491,9 @@ class SigV4Mixin(object):
|
|
444
491
|
cr.append(b';'.join(swob.wsgi_to_bytes(k) for k, v in headers_to_sign))
|
445
492
|
|
446
493
|
# 6. Add payload string at the tail
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
elif 'X-Amz-Content-SHA256' not in self.headers:
|
451
|
-
msg = 'Missing required header for this request: ' \
|
452
|
-
'x-amz-content-sha256'
|
453
|
-
raise InvalidRequest(msg)
|
454
|
-
else:
|
455
|
-
hashed_payload = self.headers['X-Amz-Content-SHA256']
|
456
|
-
if hashed_payload != 'UNSIGNED-PAYLOAD':
|
457
|
-
if self.content_length == 0:
|
458
|
-
if hashed_payload.lower() != sha256().hexdigest():
|
459
|
-
raise BadDigest(
|
460
|
-
'The X-Amz-Content-SHA56 you specified did not '
|
461
|
-
'match what we received.')
|
462
|
-
elif self.content_length:
|
463
|
-
self.environ['wsgi.input'] = HashingInput(
|
464
|
-
self.environ['wsgi.input'],
|
465
|
-
self.content_length,
|
466
|
-
sha256,
|
467
|
-
hashed_payload.lower())
|
468
|
-
# else, length not provided -- Swift will kick out a
|
469
|
-
# 411 Length Required which will get translated back
|
470
|
-
# to a S3-style response in S3Request._swift_error_codes
|
494
|
+
hashed_payload = self.headers.get('X-Amz-Content-SHA256',
|
495
|
+
'UNSIGNED-PAYLOAD')
|
496
|
+
|
471
497
|
cr.append(swob.wsgi_to_bytes(hashed_payload))
|
472
498
|
return b'\n'.join(cr)
|
473
499
|
|
@@ -549,11 +575,63 @@ class S3Request(swob.Request):
|
|
549
575
|
}
|
550
576
|
self.account = None
|
551
577
|
self.user_id = None
|
578
|
+
self.policy_index = None
|
552
579
|
|
553
580
|
# Avoids that swift.swob.Response replaces Location header value
|
554
581
|
# by full URL when absolute path given. See swift.swob for more detail.
|
555
582
|
self.environ['swift.leave_relative_location'] = True
|
556
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
|
+
|
557
635
|
def check_signature(self, secret):
|
558
636
|
secret = utf8encode(secret)
|
559
637
|
user_signature = self.signature
|
@@ -743,6 +821,9 @@ class S3Request(swob.Request):
|
|
743
821
|
if delta > self.conf.allowable_clock_skew:
|
744
822
|
raise RequestTimeTooSkewed()
|
745
823
|
|
824
|
+
def _validate_sha256(self):
|
825
|
+
return self.headers.get('x-amz-content-sha256')
|
826
|
+
|
746
827
|
def _validate_headers(self):
|
747
828
|
if 'CONTENT_LENGTH' in self.environ:
|
748
829
|
try:
|
@@ -753,21 +834,6 @@ class S3Request(swob.Request):
|
|
753
834
|
raise InvalidArgument('Content-Length',
|
754
835
|
self.environ['CONTENT_LENGTH'])
|
755
836
|
|
756
|
-
value = _header_strip(self.headers.get('Content-MD5'))
|
757
|
-
if value is not None:
|
758
|
-
if not re.match('^[A-Za-z0-9+/]+={0,2}$', value):
|
759
|
-
# Non-base64-alphabet characters in value.
|
760
|
-
raise InvalidDigest(content_md5=value)
|
761
|
-
try:
|
762
|
-
self.headers['ETag'] = binascii.b2a_hex(
|
763
|
-
binascii.a2b_base64(value))
|
764
|
-
except binascii.Error:
|
765
|
-
# incorrect padding, most likely
|
766
|
-
raise InvalidDigest(content_md5=value)
|
767
|
-
|
768
|
-
if len(self.headers['ETag']) != 32:
|
769
|
-
raise InvalidDigest(content_md5=value)
|
770
|
-
|
771
837
|
if self.method == 'PUT' and any(h in self.headers for h in (
|
772
838
|
'If-Match', 'If-None-Match',
|
773
839
|
'If-Modified-Since', 'If-Unmodified-Since')):
|
@@ -813,6 +879,38 @@ class S3Request(swob.Request):
|
|
813
879
|
if 'x-amz-website-redirect-location' in self.headers:
|
814
880
|
raise S3NotImplemented('Website redirection is not supported.')
|
815
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
|
+
|
816
914
|
# https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
|
817
915
|
# describes some of what would be required to support this
|
818
916
|
if any(['aws-chunked' in self.headers.get('content-encoding', ''),
|
@@ -854,13 +952,11 @@ class S3Request(swob.Request):
|
|
854
952
|
# Limit the read similar to how SLO handles manifests
|
855
953
|
try:
|
856
954
|
body = self.body_file.read(max_length)
|
857
|
-
except
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
'match what we received.')
|
863
|
-
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
|
+
)
|
864
960
|
else:
|
865
961
|
# No (or zero) Content-Length provided, and not chunked transfer;
|
866
962
|
# no body. Assume zero-length, and enforce a required body below.
|
@@ -919,8 +1015,6 @@ class S3Request(swob.Request):
|
|
919
1015
|
src_resp = self.get_response(app, 'HEAD', src_bucket,
|
920
1016
|
swob.str_to_wsgi(src_obj),
|
921
1017
|
headers=headers, query=query)
|
922
|
-
# we can't let this HEAD req spoil our COPY
|
923
|
-
self.headers.pop('x-backend-storage-policy-index')
|
924
1018
|
if src_resp.status_int == 304: # pylint: disable-msg=E1101
|
925
1019
|
raise PreconditionFailed()
|
926
1020
|
|
@@ -1042,7 +1136,10 @@ class S3Request(swob.Request):
|
|
1042
1136
|
if 'logging' in self.params:
|
1043
1137
|
return LoggingStatusController
|
1044
1138
|
if 'partNumber' in self.params:
|
1045
|
-
|
1139
|
+
if self.method == 'PUT':
|
1140
|
+
return PartController
|
1141
|
+
else:
|
1142
|
+
return ObjectController
|
1046
1143
|
if 'uploadId' in self.params:
|
1047
1144
|
return UploadController
|
1048
1145
|
if 'uploads' in self.params:
|
@@ -1051,6 +1148,8 @@ class S3Request(swob.Request):
|
|
1051
1148
|
return VersioningController
|
1052
1149
|
if 'tagging' in self.params:
|
1053
1150
|
return TaggingController
|
1151
|
+
if 'object-lock' in self.params:
|
1152
|
+
return ObjectLockController
|
1054
1153
|
|
1055
1154
|
unsupported = ('notification', 'policy', 'requestPayment', 'torrent',
|
1056
1155
|
'website', 'cors', 'restore')
|
@@ -1083,7 +1182,7 @@ class S3Request(swob.Request):
|
|
1083
1182
|
Create a Swift request based on this request's environment.
|
1084
1183
|
"""
|
1085
1184
|
if self.account is None:
|
1086
|
-
account = self.access_key
|
1185
|
+
account = swob.str_to_wsgi(self.access_key)
|
1087
1186
|
else:
|
1088
1187
|
account = self.account
|
1089
1188
|
|
@@ -1303,6 +1402,16 @@ class S3Request(swob.Request):
|
|
1303
1402
|
return NoSuchKey(obj)
|
1304
1403
|
return NoSuchBucket(container)
|
1305
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
|
+
|
1306
1415
|
code_map = {
|
1307
1416
|
'HEAD': {
|
1308
1417
|
HTTP_NOT_FOUND: not_found_handler,
|
@@ -1311,11 +1420,10 @@ class S3Request(swob.Request):
|
|
1311
1420
|
'GET': {
|
1312
1421
|
HTTP_NOT_FOUND: not_found_handler,
|
1313
1422
|
HTTP_PRECONDITION_FAILED: PreconditionFailed,
|
1314
|
-
HTTP_REQUESTED_RANGE_NOT_SATISFIABLE: InvalidRange,
|
1315
1423
|
},
|
1316
1424
|
'PUT': {
|
1317
1425
|
HTTP_NOT_FOUND: (NoSuchBucket, container),
|
1318
|
-
HTTP_UNPROCESSABLE_ENTITY:
|
1426
|
+
HTTP_UNPROCESSABLE_ENTITY: bad_digest_handler,
|
1319
1427
|
HTTP_REQUEST_ENTITY_TOO_LARGE: EntityTooLarge,
|
1320
1428
|
HTTP_LENGTH_REQUIRED: MissingContentLength,
|
1321
1429
|
HTTP_REQUEST_TIMEOUT: RequestTimeout,
|
@@ -1352,24 +1460,25 @@ class S3Request(swob.Request):
|
|
1352
1460
|
|
1353
1461
|
try:
|
1354
1462
|
sw_resp = sw_req.get_response(app)
|
1355
|
-
except
|
1356
|
-
#
|
1357
|
-
#
|
1358
|
-
# path (e.g. tenant to account translation) will have been made by
|
1359
|
-
# 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
|
1360
1466
|
self.environ['s3api.backend_path'] = sw_req.environ['PATH_INFO']
|
1361
|
-
|
1467
|
+
raise XAmzContentSHA256Mismatch(
|
1468
|
+
client_computed_content_s_h_a256=err.expected,
|
1469
|
+
s3_computed_content_s_h_a256=err.computed,
|
1470
|
+
)
|
1362
1471
|
else:
|
1363
1472
|
# reuse account
|
1364
1473
|
_, self.account, _ = split_path(sw_resp.environ['PATH_INFO'],
|
1365
1474
|
2, 3, True)
|
1366
1475
|
# Update s3.backend_path from the response environ
|
1367
1476
|
self.environ['s3api.backend_path'] = sw_resp.environ['PATH_INFO']
|
1368
|
-
# Propogate backend headers back into our req headers for logging
|
1369
|
-
for k, v in sw_req.headers.items():
|
1370
|
-
if k.lower().startswith('x-backend-'):
|
1371
|
-
self.headers.setdefault(k, v)
|
1372
1477
|
|
1478
|
+
# keep a record of the backend policy index so that the s3api can add
|
1479
|
+
# it to the headers of whatever response it returns, which may not
|
1480
|
+
# necessarily be this resp.
|
1481
|
+
self.policy_index = get_policy_index(sw_req.headers, sw_resp.headers)
|
1373
1482
|
resp = S3Response.from_swift_resp(sw_resp)
|
1374
1483
|
status = resp.status_int # pylint: disable-msg=E1101
|
1375
1484
|
|
@@ -1410,7 +1519,7 @@ class S3Request(swob.Request):
|
|
1410
1519
|
raise InvalidArgument('X-Delete-At',
|
1411
1520
|
self.headers['X-Delete-At'],
|
1412
1521
|
err_str)
|
1413
|
-
if 'X-Delete-After' in
|
1522
|
+
if 'X-Delete-After' in err_str:
|
1414
1523
|
raise InvalidArgument('X-Delete-After',
|
1415
1524
|
self.headers['X-Delete-After'],
|
1416
1525
|
err_str)
|
@@ -1421,6 +1530,10 @@ class S3Request(swob.Request):
|
|
1421
1530
|
**self.signature_does_not_match_kwargs())
|
1422
1531
|
if status == HTTP_FORBIDDEN:
|
1423
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()
|
1424
1537
|
if status == HTTP_SERVICE_UNAVAILABLE:
|
1425
1538
|
raise ServiceUnavailable()
|
1426
1539
|
if status in (HTTP_RATE_LIMITED, HTTP_TOO_MANY_REQUESTS):
|
@@ -1428,8 +1541,10 @@ class S3Request(swob.Request):
|
|
1428
1541
|
raise SlowDown(status='429 Slow Down')
|
1429
1542
|
raise SlowDown()
|
1430
1543
|
if resp.status_int == HTTP_CONFLICT:
|
1431
|
-
|
1432
|
-
|
1544
|
+
if self.method == 'GET':
|
1545
|
+
raise BrokenMPU()
|
1546
|
+
else:
|
1547
|
+
raise ServiceUnavailable()
|
1433
1548
|
|
1434
1549
|
raise InternalError('unexpected status code %d' % status)
|
1435
1550
|
|
@@ -1497,7 +1612,7 @@ class S3Request(swob.Request):
|
|
1497
1612
|
|
1498
1613
|
_, self.account, _ = split_path(sw_resp.environ['PATH_INFO'],
|
1499
1614
|
2, 3, True)
|
1500
|
-
sw_req = self.to_swift_req(
|
1615
|
+
sw_req = self.to_swift_req('TEST', self.container_name, None)
|
1501
1616
|
info = get_container_info(sw_req.environ, app, swift_source='S3')
|
1502
1617
|
if is_success(info['status']):
|
1503
1618
|
return info
|
@@ -1536,6 +1651,7 @@ class S3AclRequest(S3Request):
|
|
1536
1651
|
"""
|
1537
1652
|
S3Acl request object.
|
1538
1653
|
"""
|
1654
|
+
|
1539
1655
|
def __init__(self, env, app=None, conf=None):
|
1540
1656
|
super(S3AclRequest, self).__init__(env, app, conf)
|
1541
1657
|
self.authenticate(app)
|
@@ -64,7 +64,7 @@ def translate_swift_to_s3(key, val):
|
|
64
64
|
|
65
65
|
if _key.startswith('x-object-meta-'):
|
66
66
|
return translate_meta_key(_key), val
|
67
|
-
elif _key in ('content-length', 'content-type',
|
67
|
+
elif _key in ('accept-ranges', 'content-length', 'content-type',
|
68
68
|
'content-range', 'content-encoding',
|
69
69
|
'content-disposition', 'content-language',
|
70
70
|
'etag', 'last-modified', 'x-robots-tag',
|
@@ -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 \
|
@@ -111,6 +113,7 @@ class S3Response(S3ResponseBase, swob.Response):
|
|
111
113
|
headers instead of Swift's HeaderKeyDict. This also translates Swift
|
112
114
|
specific headers to S3 headers.
|
113
115
|
"""
|
116
|
+
|
114
117
|
def __init__(self, *args, **kwargs):
|
115
118
|
swob.Response.__init__(self, *args, **kwargs)
|
116
119
|
|
@@ -239,7 +242,7 @@ class ErrorResponse(S3ResponseBase, swob.HTTPException):
|
|
239
242
|
self.info = kwargs.copy()
|
240
243
|
for reserved_key in ('headers', 'body'):
|
241
244
|
if self.info.get(reserved_key):
|
242
|
-
del(self.info[reserved_key])
|
245
|
+
del (self.info[reserved_key])
|
243
246
|
|
244
247
|
swob.HTTPException.__init__(
|
245
248
|
self, status=kwargs.pop('status', self._status),
|
@@ -249,12 +252,17 @@ class ErrorResponse(S3ResponseBase, swob.HTTPException):
|
|
249
252
|
self.headers = HeaderKeyDict(self.headers)
|
250
253
|
|
251
254
|
@property
|
252
|
-
def
|
253
|
-
|
255
|
+
def summary(self):
|
256
|
+
"""Provide a summary of the error code and reason."""
|
254
257
|
if self.reason:
|
255
|
-
|
256
|
-
|
257
|
-
|
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])
|
258
266
|
|
259
267
|
def _body_iter(self):
|
260
268
|
error_elem = Element('Error')
|
@@ -319,6 +327,12 @@ class BadDigest(ErrorResponse):
|
|
319
327
|
_msg = 'The Content-MD5 you specified did not match what we received.'
|
320
328
|
|
321
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
|
+
|
322
336
|
class BucketAlreadyExists(ErrorResponse):
|
323
337
|
_status = '409 Conflict'
|
324
338
|
_msg = 'The requested bucket name is not available. The bucket ' \
|
@@ -435,7 +449,7 @@ class InvalidBucketState(ErrorResponse):
|
|
435
449
|
|
436
450
|
class InvalidDigest(ErrorResponse):
|
437
451
|
_status = '400 Bad Request'
|
438
|
-
_msg = 'The Content-MD5 you specified was
|
452
|
+
_msg = 'The Content-MD5 you specified was invalid.'
|
439
453
|
|
440
454
|
|
441
455
|
class InvalidLocationConstraint(ErrorResponse):
|
@@ -448,6 +462,17 @@ class InvalidObjectState(ErrorResponse):
|
|
448
462
|
_msg = 'The operation is not valid for the current state of the object.'
|
449
463
|
|
450
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
|
+
|
451
476
|
class InvalidPart(ErrorResponse):
|
452
477
|
_status = '400 Bad Request'
|
453
478
|
_msg = 'One or more of the specified parts could not be found. The part ' \
|
@@ -477,6 +502,11 @@ class InvalidRange(ErrorResponse):
|
|
477
502
|
_msg = 'The requested range cannot be satisfied.'
|
478
503
|
|
479
504
|
|
505
|
+
class InvalidPartNumber(ErrorResponse):
|
506
|
+
_status = '416 Requested Range Not Satisfiable'
|
507
|
+
_msg = 'The requested partnumber is not satisfiable'
|
508
|
+
|
509
|
+
|
480
510
|
class InvalidRequest(ErrorResponse):
|
481
511
|
_status = '400 Bad Request'
|
482
512
|
_msg = 'Invalid Request.'
|
@@ -613,6 +643,16 @@ class NoSuchKey(ErrorResponse):
|
|
613
643
|
ErrorResponse.__init__(self, msg, key=key, *args, **kwargs)
|
614
644
|
|
615
645
|
|
646
|
+
class ObjectLockConfigurationNotFoundError(ErrorResponse):
|
647
|
+
_status = '404 Not found'
|
648
|
+
_msg = 'Object Lock configuration does not exist for this bucket'
|
649
|
+
|
650
|
+
def __init__(self, bucket, msg=None, *args, **kwargs):
|
651
|
+
if not bucket:
|
652
|
+
raise InternalError()
|
653
|
+
ErrorResponse.__init__(self, msg, bucket_name=bucket, *args, **kwargs)
|
654
|
+
|
655
|
+
|
616
656
|
class NoSuchLifecycleConfiguration(ErrorResponse):
|
617
657
|
_status = '404 Not Found'
|
618
658
|
_msg = 'The lifecycle configuration does not exist. .'
|
@@ -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
|
|
@@ -113,7 +113,7 @@ class S3Timestamp(utils.Timestamp):
|
|
113
113
|
|
114
114
|
@property
|
115
115
|
def s3xmlformat(self):
|
116
|
-
dt = datetime.datetime.
|
116
|
+
dt = datetime.datetime.fromtimestamp(self.ceil(), utils.UTC)
|
117
117
|
return dt.strftime(self.S3_XML_FORMAT)
|
118
118
|
|
119
119
|
@classmethod
|
@@ -172,6 +172,7 @@ class Config(dict):
|
|
172
172
|
'allow_no_owner': False,
|
173
173
|
'allowable_clock_skew': 900,
|
174
174
|
'ratelimit_as_client_error': False,
|
175
|
+
'max_upload_part_num': 1000,
|
175
176
|
}
|
176
177
|
|
177
178
|
def __init__(self, base=None):
|