swift 2.23.2__py3-none-any.whl → 2.35.0__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/__init__.py +29 -50
- swift/account/auditor.py +21 -118
- swift/account/backend.py +33 -28
- swift/account/reaper.py +37 -28
- swift/account/replicator.py +22 -0
- swift/account/server.py +60 -26
- swift/account/utils.py +28 -11
- swift-2.23.2.data/scripts/swift-account-audit → swift/cli/account_audit.py +23 -13
- swift-2.23.2.data/scripts/swift-config → swift/cli/config.py +2 -2
- swift/cli/container_deleter.py +5 -11
- swift-2.23.2.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +8 -7
- swift/cli/dispersion_report.py +10 -9
- swift-2.23.2.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +63 -21
- swift/cli/form_signature.py +3 -7
- swift-2.23.2.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +8 -2
- swift/cli/info.py +183 -29
- swift/cli/manage_shard_ranges.py +708 -37
- swift-2.23.2.data/scripts/swift-oldies → swift/cli/oldies.py +25 -14
- swift-2.23.2.data/scripts/swift-orphans → swift/cli/orphans.py +7 -3
- swift/cli/recon.py +196 -67
- swift-2.23.2.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +17 -20
- swift-2.23.2.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
- swift/cli/relinker.py +807 -126
- swift/cli/reload.py +135 -0
- swift/cli/ringbuilder.py +217 -20
- swift/cli/ringcomposer.py +0 -1
- swift/cli/shard-info.py +4 -3
- swift/common/base_storage_server.py +9 -20
- swift/common/bufferedhttp.py +48 -74
- swift/common/constraints.py +20 -15
- swift/common/container_sync_realms.py +9 -11
- swift/common/daemon.py +25 -8
- swift/common/db.py +198 -127
- swift/common/db_auditor.py +168 -0
- swift/common/db_replicator.py +95 -55
- swift/common/digest.py +141 -0
- swift/common/direct_client.py +144 -33
- swift/common/error_limiter.py +93 -0
- swift/common/exceptions.py +25 -1
- swift/common/header_key_dict.py +2 -9
- swift/common/http_protocol.py +373 -0
- swift/common/internal_client.py +129 -59
- swift/common/linkat.py +3 -4
- swift/common/manager.py +284 -67
- swift/common/memcached.py +396 -147
- swift/common/middleware/__init__.py +4 -0
- swift/common/middleware/account_quotas.py +211 -46
- swift/common/middleware/acl.py +3 -8
- swift/common/middleware/backend_ratelimit.py +230 -0
- swift/common/middleware/bulk.py +22 -34
- swift/common/middleware/catch_errors.py +1 -3
- swift/common/middleware/cname_lookup.py +6 -11
- swift/common/middleware/container_quotas.py +1 -1
- swift/common/middleware/container_sync.py +39 -17
- swift/common/middleware/copy.py +12 -0
- swift/common/middleware/crossdomain.py +22 -9
- swift/common/middleware/crypto/__init__.py +2 -1
- swift/common/middleware/crypto/crypto_utils.py +11 -15
- swift/common/middleware/crypto/decrypter.py +28 -11
- swift/common/middleware/crypto/encrypter.py +12 -17
- swift/common/middleware/crypto/keymaster.py +8 -15
- swift/common/middleware/crypto/kms_keymaster.py +2 -1
- swift/common/middleware/dlo.py +15 -11
- swift/common/middleware/domain_remap.py +5 -4
- swift/common/middleware/etag_quoter.py +128 -0
- swift/common/middleware/formpost.py +73 -70
- swift/common/middleware/gatekeeper.py +8 -1
- swift/common/middleware/keystoneauth.py +33 -3
- swift/common/middleware/list_endpoints.py +4 -4
- swift/common/middleware/listing_formats.py +85 -49
- swift/common/middleware/memcache.py +4 -81
- swift/common/middleware/name_check.py +3 -2
- swift/common/middleware/proxy_logging.py +160 -92
- swift/common/middleware/ratelimit.py +17 -10
- swift/common/middleware/read_only.py +6 -4
- swift/common/middleware/recon.py +59 -22
- swift/common/middleware/s3api/acl_handlers.py +25 -3
- swift/common/middleware/s3api/acl_utils.py +6 -1
- swift/common/middleware/s3api/controllers/__init__.py +6 -0
- swift/common/middleware/s3api/controllers/acl.py +3 -2
- swift/common/middleware/s3api/controllers/bucket.py +242 -137
- swift/common/middleware/s3api/controllers/logging.py +2 -2
- swift/common/middleware/s3api/controllers/multi_delete.py +43 -20
- swift/common/middleware/s3api/controllers/multi_upload.py +219 -133
- swift/common/middleware/s3api/controllers/obj.py +112 -8
- swift/common/middleware/s3api/controllers/object_lock.py +44 -0
- swift/common/middleware/s3api/controllers/s3_acl.py +2 -2
- swift/common/middleware/s3api/controllers/tagging.py +57 -0
- swift/common/middleware/s3api/controllers/versioning.py +36 -7
- swift/common/middleware/s3api/etree.py +22 -9
- swift/common/middleware/s3api/exception.py +0 -4
- swift/common/middleware/s3api/s3api.py +113 -41
- swift/common/middleware/s3api/s3request.py +384 -218
- swift/common/middleware/s3api/s3response.py +126 -23
- swift/common/middleware/s3api/s3token.py +16 -17
- swift/common/middleware/s3api/schema/delete.rng +1 -1
- swift/common/middleware/s3api/subresource.py +7 -10
- swift/common/middleware/s3api/utils.py +27 -10
- swift/common/middleware/slo.py +665 -358
- swift/common/middleware/staticweb.py +64 -37
- swift/common/middleware/symlink.py +52 -19
- swift/common/middleware/tempauth.py +76 -58
- swift/common/middleware/tempurl.py +192 -174
- swift/common/middleware/versioned_writes/__init__.py +51 -0
- swift/common/middleware/{versioned_writes.py → versioned_writes/legacy.py} +27 -26
- swift/common/middleware/versioned_writes/object_versioning.py +1482 -0
- swift/common/middleware/x_profile/exceptions.py +1 -4
- swift/common/middleware/x_profile/html_viewer.py +18 -19
- swift/common/middleware/x_profile/profile_model.py +1 -2
- swift/common/middleware/xprofile.py +10 -10
- swift-2.23.2.data/scripts/swift-container-server → swift/common/recon.py +13 -8
- swift/common/registry.py +147 -0
- swift/common/request_helpers.py +324 -57
- swift/common/ring/builder.py +67 -25
- swift/common/ring/composite_builder.py +1 -1
- swift/common/ring/ring.py +177 -51
- swift/common/ring/utils.py +1 -1
- swift/common/splice.py +10 -6
- swift/common/statsd_client.py +205 -0
- swift/common/storage_policy.py +49 -44
- swift/common/swob.py +86 -102
- swift/common/{utils.py → utils/__init__.py} +2191 -2762
- swift/common/utils/base.py +131 -0
- swift/common/utils/config.py +433 -0
- swift/common/utils/ipaddrs.py +256 -0
- swift/common/utils/libc.py +345 -0
- swift/common/utils/logs.py +859 -0
- swift/common/utils/timestamp.py +412 -0
- swift/common/wsgi.py +555 -536
- swift/container/auditor.py +14 -100
- swift/container/backend.py +552 -227
- swift/container/reconciler.py +126 -37
- swift/container/replicator.py +96 -22
- swift/container/server.py +397 -176
- swift/container/sharder.py +1580 -639
- swift/container/sync.py +94 -88
- swift/container/updater.py +53 -32
- swift/obj/auditor.py +153 -35
- swift/obj/diskfile.py +466 -217
- swift/obj/expirer.py +406 -124
- swift/obj/mem_diskfile.py +7 -4
- swift/obj/mem_server.py +1 -0
- swift/obj/reconstructor.py +523 -262
- swift/obj/replicator.py +249 -188
- swift/obj/server.py +213 -122
- swift/obj/ssync_receiver.py +145 -85
- swift/obj/ssync_sender.py +113 -54
- swift/obj/updater.py +653 -139
- swift/obj/watchers/__init__.py +0 -0
- swift/obj/watchers/dark_data.py +213 -0
- swift/proxy/controllers/account.py +11 -11
- swift/proxy/controllers/base.py +848 -604
- swift/proxy/controllers/container.py +452 -86
- swift/proxy/controllers/info.py +3 -2
- swift/proxy/controllers/obj.py +1009 -490
- swift/proxy/server.py +185 -112
- swift-2.35.0.dist-info/AUTHORS +501 -0
- swift-2.35.0.dist-info/LICENSE +202 -0
- {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/METADATA +52 -61
- swift-2.35.0.dist-info/RECORD +201 -0
- {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/WHEEL +1 -1
- {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/entry_points.txt +43 -0
- swift-2.35.0.dist-info/pbr.json +1 -0
- swift/locale/de/LC_MESSAGES/swift.po +0 -1216
- swift/locale/en_GB/LC_MESSAGES/swift.po +0 -1207
- swift/locale/es/LC_MESSAGES/swift.po +0 -1085
- swift/locale/fr/LC_MESSAGES/swift.po +0 -909
- swift/locale/it/LC_MESSAGES/swift.po +0 -894
- swift/locale/ja/LC_MESSAGES/swift.po +0 -965
- swift/locale/ko_KR/LC_MESSAGES/swift.po +0 -964
- swift/locale/pt_BR/LC_MESSAGES/swift.po +0 -881
- swift/locale/ru/LC_MESSAGES/swift.po +0 -891
- swift/locale/tr_TR/LC_MESSAGES/swift.po +0 -832
- swift/locale/zh_CN/LC_MESSAGES/swift.po +0 -833
- swift/locale/zh_TW/LC_MESSAGES/swift.po +0 -838
- swift-2.23.2.data/scripts/swift-account-auditor +0 -23
- swift-2.23.2.data/scripts/swift-account-info +0 -51
- swift-2.23.2.data/scripts/swift-account-reaper +0 -23
- swift-2.23.2.data/scripts/swift-account-replicator +0 -34
- swift-2.23.2.data/scripts/swift-account-server +0 -23
- swift-2.23.2.data/scripts/swift-container-auditor +0 -23
- swift-2.23.2.data/scripts/swift-container-info +0 -51
- swift-2.23.2.data/scripts/swift-container-reconciler +0 -21
- swift-2.23.2.data/scripts/swift-container-replicator +0 -34
- swift-2.23.2.data/scripts/swift-container-sharder +0 -33
- swift-2.23.2.data/scripts/swift-container-sync +0 -23
- swift-2.23.2.data/scripts/swift-container-updater +0 -23
- swift-2.23.2.data/scripts/swift-dispersion-report +0 -24
- swift-2.23.2.data/scripts/swift-form-signature +0 -20
- swift-2.23.2.data/scripts/swift-init +0 -119
- swift-2.23.2.data/scripts/swift-object-auditor +0 -29
- swift-2.23.2.data/scripts/swift-object-expirer +0 -33
- swift-2.23.2.data/scripts/swift-object-info +0 -60
- swift-2.23.2.data/scripts/swift-object-reconstructor +0 -33
- swift-2.23.2.data/scripts/swift-object-relinker +0 -41
- swift-2.23.2.data/scripts/swift-object-replicator +0 -37
- swift-2.23.2.data/scripts/swift-object-server +0 -27
- swift-2.23.2.data/scripts/swift-object-updater +0 -23
- swift-2.23.2.data/scripts/swift-proxy-server +0 -23
- swift-2.23.2.data/scripts/swift-recon +0 -24
- swift-2.23.2.data/scripts/swift-ring-builder +0 -24
- swift-2.23.2.data/scripts/swift-ring-builder-analyzer +0 -22
- swift-2.23.2.data/scripts/swift-ring-composer +0 -22
- swift-2.23.2.dist-info/DESCRIPTION.rst +0 -166
- swift-2.23.2.dist-info/RECORD +0 -220
- swift-2.23.2.dist-info/metadata.json +0 -1
- swift-2.23.2.dist-info/pbr.json +0 -1
- {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/top_level.txt +0 -0
@@ -17,16 +17,16 @@ import base64
|
|
17
17
|
import binascii
|
18
18
|
from collections import defaultdict, OrderedDict
|
19
19
|
from email.header import Header
|
20
|
-
from hashlib import sha1, sha256
|
20
|
+
from hashlib import sha1, sha256
|
21
21
|
import hmac
|
22
22
|
import re
|
23
|
-
import six
|
24
23
|
# pylint: disable-msg=import-error
|
25
|
-
from
|
24
|
+
from urllib.parse import quote, unquote, parse_qsl
|
26
25
|
import string
|
27
26
|
|
28
|
-
from swift.common.utils import split_path, json,
|
29
|
-
|
27
|
+
from swift.common.utils import split_path, json, md5, streq_const_time, \
|
28
|
+
get_policy_index, InputProxy
|
29
|
+
from swift.common.registry import get_swift_info
|
30
30
|
from swift.common import swob
|
31
31
|
from swift.common.http import HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, \
|
32
32
|
HTTP_NO_CONTENT, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, \
|
@@ -34,18 +34,19 @@ from swift.common.http import HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, \
|
|
34
34
|
HTTP_PARTIAL_CONTENT, HTTP_NOT_MODIFIED, HTTP_PRECONDITION_FAILED, \
|
35
35
|
HTTP_REQUESTED_RANGE_NOT_SATISFIABLE, HTTP_LENGTH_REQUIRED, \
|
36
36
|
HTTP_BAD_REQUEST, HTTP_REQUEST_TIMEOUT, HTTP_SERVICE_UNAVAILABLE, \
|
37
|
-
HTTP_TOO_MANY_REQUESTS, HTTP_RATE_LIMITED, is_success
|
37
|
+
HTTP_TOO_MANY_REQUESTS, HTTP_RATE_LIMITED, is_success, \
|
38
|
+
HTTP_CLIENT_CLOSED_REQUEST
|
38
39
|
|
39
40
|
from swift.common.constraints import check_utf8
|
40
|
-
from swift.proxy.controllers.base import get_container_info
|
41
|
-
headers_to_container_info
|
41
|
+
from swift.proxy.controllers.base import get_container_info
|
42
42
|
from swift.common.request_helpers import check_path_header
|
43
43
|
|
44
44
|
from swift.common.middleware.s3api.controllers import ServiceController, \
|
45
45
|
ObjectController, AclController, MultiObjectDeleteController, \
|
46
46
|
LocationController, LoggingStatusController, PartController, \
|
47
47
|
UploadController, UploadsController, VersioningController, \
|
48
|
-
UnsupportedController, S3AclController, BucketController
|
48
|
+
UnsupportedController, S3AclController, BucketController, \
|
49
|
+
TaggingController, ObjectLockController
|
49
50
|
from swift.common.middleware.s3api.s3response import AccessDenied, \
|
50
51
|
InvalidArgument, InvalidDigest, BucketAlreadyOwnedByYou, \
|
51
52
|
RequestTimeTooSkewed, S3Response, SignatureDoesNotMatch, \
|
@@ -54,14 +55,14 @@ from swift.common.middleware.s3api.s3response import AccessDenied, \
|
|
54
55
|
MissingContentLength, InvalidStorageClass, S3NotImplemented, InvalidURI, \
|
55
56
|
MalformedXML, InvalidRequest, RequestTimeout, InvalidBucketName, \
|
56
57
|
BadDigest, AuthorizationHeaderMalformed, SlowDown, \
|
57
|
-
AuthorizationQueryParametersError, ServiceUnavailable
|
58
|
-
|
59
|
-
|
58
|
+
AuthorizationQueryParametersError, ServiceUnavailable, BrokenMPU, \
|
59
|
+
InvalidPartNumber, InvalidPartArgument, XAmzContentSHA256Mismatch
|
60
|
+
from swift.common.middleware.s3api.exception import NotS3Request
|
60
61
|
from swift.common.middleware.s3api.utils import utf8encode, \
|
61
62
|
S3Timestamp, mktime, MULTIUPLOAD_SUFFIX
|
62
63
|
from swift.common.middleware.s3api.subresource import decode_acl, encode_acl
|
63
64
|
from swift.common.middleware.s3api.utils import sysmeta_header, \
|
64
|
-
validate_bucket_name
|
65
|
+
validate_bucket_name, Config
|
65
66
|
from swift.common.middleware.s3api.acl_utils import handle_acl_header
|
66
67
|
|
67
68
|
|
@@ -73,7 +74,8 @@ ALLOWED_SUB_RESOURCES = sorted([
|
|
73
74
|
'versionId', 'versioning', 'versions', 'website',
|
74
75
|
'response-cache-control', 'response-content-disposition',
|
75
76
|
'response-content-encoding', 'response-content-language',
|
76
|
-
'response-content-type', 'response-expires', 'cors', 'tagging', 'restore'
|
77
|
+
'response-content-type', 'response-expires', 'cors', 'tagging', 'restore',
|
78
|
+
'object-lock'
|
77
79
|
])
|
78
80
|
|
79
81
|
|
@@ -102,6 +104,7 @@ def _header_acl_property(resource):
|
|
102
104
|
"""
|
103
105
|
Set and retrieve the acl in self.headers
|
104
106
|
"""
|
107
|
+
|
105
108
|
def getter(self):
|
106
109
|
return getattr(self, '_%s' % resource)
|
107
110
|
|
@@ -116,33 +119,56 @@ def _header_acl_property(resource):
|
|
116
119
|
doc='Get and set the %s acl property' % resource)
|
117
120
|
|
118
121
|
|
119
|
-
class
|
122
|
+
class S3InputSHA256Mismatch(BaseException):
|
123
|
+
"""
|
124
|
+
Client provided a X-Amz-Content-SHA256, but it doesn't match the data.
|
125
|
+
|
126
|
+
Inherit from BaseException (rather than Exception) so it cuts from the
|
127
|
+
proxy-server app (which will presumably be the one reading the input)
|
128
|
+
through all the layers of the pipeline back to us. It should never escape
|
129
|
+
the s3api middleware.
|
130
|
+
"""
|
131
|
+
def __init__(self, expected, computed):
|
132
|
+
self.expected = expected
|
133
|
+
self.computed = computed
|
134
|
+
|
135
|
+
|
136
|
+
class HashingInput(InputProxy):
|
120
137
|
"""
|
121
|
-
wsgi.input wrapper to verify the
|
138
|
+
wsgi.input wrapper to verify the SHA256 of the input as it's read.
|
122
139
|
"""
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
self.
|
127
|
-
self.
|
128
|
-
|
129
|
-
|
130
|
-
|
140
|
+
|
141
|
+
def __init__(self, wsgi_input, content_length, expected_hex_hash):
|
142
|
+
super().__init__(wsgi_input)
|
143
|
+
self._expected_length = content_length
|
144
|
+
self._hasher = sha256()
|
145
|
+
self._expected_hash = expected_hex_hash
|
146
|
+
if content_length == 0 and \
|
147
|
+
self._hasher.hexdigest() != self._expected_hash.lower():
|
148
|
+
self.close()
|
149
|
+
raise XAmzContentSHA256Mismatch(
|
150
|
+
client_computed_content_s_h_a256=self._expected_hash,
|
151
|
+
s3_computed_content_s_h_a256=self._hasher.hexdigest(),
|
152
|
+
)
|
153
|
+
|
154
|
+
def chunk_update(self, chunk, eof, *args, **kwargs):
|
131
155
|
self._hasher.update(chunk)
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
156
|
+
|
157
|
+
if self.bytes_received < self._expected_length:
|
158
|
+
error = eof
|
159
|
+
elif self.bytes_received == self._expected_length:
|
160
|
+
error = self._hasher.hexdigest() != self._expected_hash.lower()
|
161
|
+
else:
|
162
|
+
error = True
|
163
|
+
|
164
|
+
if error:
|
137
165
|
self.close()
|
138
166
|
# Since we don't return the last chunk, the PUT never completes
|
139
|
-
raise
|
140
|
-
|
141
|
-
|
142
|
-
return chunk
|
167
|
+
raise S3InputSHA256Mismatch(
|
168
|
+
self._expected_hash,
|
169
|
+
self._hasher.hexdigest())
|
143
170
|
|
144
|
-
|
145
|
-
close_if_possible(self._input)
|
171
|
+
return chunk
|
146
172
|
|
147
173
|
|
148
174
|
class SigV4Mixin(object):
|
@@ -159,7 +185,7 @@ class SigV4Mixin(object):
|
|
159
185
|
derived_secret, scope_piece.encode('utf8'), sha256).digest()
|
160
186
|
valid_signature = hmac.new(
|
161
187
|
derived_secret, self.string_to_sign, sha256).hexdigest()
|
162
|
-
return user_signature
|
188
|
+
return streq_const_time(user_signature, valid_signature)
|
163
189
|
|
164
190
|
@property
|
165
191
|
def _is_query_auth(self):
|
@@ -188,11 +214,13 @@ class SigV4Mixin(object):
|
|
188
214
|
timestamp = mktime(self.headers.get('Date'))
|
189
215
|
except (ValueError, TypeError):
|
190
216
|
raise AccessDenied('AWS authentication requires a valid Date '
|
191
|
-
'or x-amz-date header'
|
217
|
+
'or x-amz-date header',
|
218
|
+
reason='invalid_date')
|
192
219
|
|
193
220
|
if timestamp < 0:
|
194
221
|
raise AccessDenied('AWS authentication requires a valid Date '
|
195
|
-
'or x-amz-date header'
|
222
|
+
'or x-amz-date header',
|
223
|
+
reason='invalid_date')
|
196
224
|
|
197
225
|
try:
|
198
226
|
self._timestamp = S3Timestamp(timestamp)
|
@@ -213,7 +241,7 @@ class SigV4Mixin(object):
|
|
213
241
|
try:
|
214
242
|
expires = int(self.params['X-Amz-Expires'])
|
215
243
|
except KeyError:
|
216
|
-
raise AccessDenied()
|
244
|
+
raise AccessDenied(reason='invalid_expires')
|
217
245
|
except ValueError:
|
218
246
|
err = 'X-Amz-Expires should be a number'
|
219
247
|
else:
|
@@ -229,14 +257,36 @@ class SigV4Mixin(object):
|
|
229
257
|
raise AuthorizationQueryParametersError(err)
|
230
258
|
|
231
259
|
if int(self.timestamp) + expires < S3Timestamp.now():
|
232
|
-
raise AccessDenied('Request has expired')
|
260
|
+
raise AccessDenied('Request has expired', reason='expired')
|
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
|
233
283
|
|
234
284
|
def _parse_credential(self, credential_string):
|
235
285
|
parts = credential_string.split("/")
|
236
286
|
# credential must be in following format:
|
237
287
|
# <access-key-id>/<date>/<AWS-region>/<AWS-service>/aws4_request
|
238
288
|
if not parts[0] or len(parts) != 5:
|
239
|
-
raise AccessDenied()
|
289
|
+
raise AccessDenied(reason='invalid_credential')
|
240
290
|
return dict(zip(['access', 'date', 'region', 'service', 'terminal'],
|
241
291
|
parts))
|
242
292
|
|
@@ -253,15 +303,16 @@ class SigV4Mixin(object):
|
|
253
303
|
self.params.get('X-Amz-Algorithm'))
|
254
304
|
try:
|
255
305
|
cred_param = self._parse_credential(
|
256
|
-
self.params['X-Amz-Credential'])
|
257
|
-
sig = self.params['X-Amz-Signature']
|
306
|
+
swob.wsgi_to_str(self.params['X-Amz-Credential']))
|
307
|
+
sig = swob.wsgi_to_str(self.params['X-Amz-Signature'])
|
258
308
|
if not sig:
|
259
|
-
raise AccessDenied()
|
309
|
+
raise AccessDenied(reason='invalid_query_auth')
|
260
310
|
except KeyError:
|
261
|
-
raise AccessDenied()
|
311
|
+
raise AccessDenied(reason='invalid_query_auth')
|
262
312
|
|
263
313
|
try:
|
264
|
-
signed_headers =
|
314
|
+
signed_headers = swob.wsgi_to_str(
|
315
|
+
self.params['X-Amz-SignedHeaders'])
|
265
316
|
except KeyError:
|
266
317
|
# TODO: make sure if is it malformed request?
|
267
318
|
raise AuthorizationHeaderMalformed()
|
@@ -304,12 +355,12 @@ class SigV4Mixin(object):
|
|
304
355
|
:raises: AuthorizationHeaderMalformed
|
305
356
|
"""
|
306
357
|
|
307
|
-
auth_str = self.headers['Authorization']
|
358
|
+
auth_str = swob.wsgi_to_str(self.headers['Authorization'])
|
308
359
|
cred_param = self._parse_credential(auth_str.partition(
|
309
360
|
"Credential=")[2].split(',')[0])
|
310
361
|
sig = auth_str.partition("Signature=")[2].split(',')[0]
|
311
362
|
if not sig:
|
312
|
-
raise AccessDenied()
|
363
|
+
raise AccessDenied(reason='invalid_header_auth')
|
313
364
|
signed_headers = auth_str.partition(
|
314
365
|
"SignedHeaders=")[2].split(',', 1)[0]
|
315
366
|
if not signed_headers:
|
@@ -370,7 +421,7 @@ class SigV4Mixin(object):
|
|
370
421
|
else: # mostly-functional fallback
|
371
422
|
headers_lower_dict = dict(
|
372
423
|
(k.lower().strip(), ' '.join(_header_strip(v or '').split()))
|
373
|
-
for (k, v) in
|
424
|
+
for (k, v) in self.headers.items())
|
374
425
|
|
375
426
|
if 'host' in headers_lower_dict and re.match(
|
376
427
|
'Boto/2.[0-9].[0-2]',
|
@@ -383,7 +434,7 @@ class SigV4Mixin(object):
|
|
383
434
|
|
384
435
|
headers_to_sign = [
|
385
436
|
(key, value) for key, value in sorted(headers_lower_dict.items())
|
386
|
-
if key in self._signed_headers]
|
437
|
+
if swob.wsgi_to_str(key) in self._signed_headers]
|
387
438
|
|
388
439
|
if len(headers_to_sign) != len(self._signed_headers):
|
389
440
|
# NOTE: if we are missing the header suggested via
|
@@ -399,7 +450,8 @@ class SigV4Mixin(object):
|
|
399
450
|
"""
|
400
451
|
It won't require bucket name in canonical_uri for v4.
|
401
452
|
"""
|
402
|
-
return swob.wsgi_to_bytes(
|
453
|
+
return swob.wsgi_to_bytes(swob.wsgi_quote(
|
454
|
+
self.environ.get('PATH_INFO', self.path), safe='-_.~/'))
|
403
455
|
|
404
456
|
def _canonical_request(self):
|
405
457
|
# prepare 'canonical_request'
|
@@ -417,7 +469,7 @@ class SigV4Mixin(object):
|
|
417
469
|
#
|
418
470
|
|
419
471
|
# 1. Add verb like: GET
|
420
|
-
cr = [swob.wsgi_to_bytes(self.method
|
472
|
+
cr = [swob.wsgi_to_bytes(self.method)]
|
421
473
|
|
422
474
|
# 2. Add path like: /
|
423
475
|
path = self._canonical_uri()
|
@@ -439,30 +491,9 @@ class SigV4Mixin(object):
|
|
439
491
|
cr.append(b';'.join(swob.wsgi_to_bytes(k) for k, v in headers_to_sign))
|
440
492
|
|
441
493
|
# 6. Add payload string at the tail
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
elif 'X-Amz-Content-SHA256' not in self.headers:
|
446
|
-
msg = 'Missing required header for this request: ' \
|
447
|
-
'x-amz-content-sha256'
|
448
|
-
raise InvalidRequest(msg)
|
449
|
-
else:
|
450
|
-
hashed_payload = self.headers['X-Amz-Content-SHA256']
|
451
|
-
if hashed_payload != 'UNSIGNED-PAYLOAD':
|
452
|
-
if self.content_length == 0:
|
453
|
-
if hashed_payload != sha256().hexdigest():
|
454
|
-
raise BadDigest(
|
455
|
-
'The X-Amz-Content-SHA56 you specified did not '
|
456
|
-
'match what we received.')
|
457
|
-
elif self.content_length:
|
458
|
-
self.environ['wsgi.input'] = HashingInput(
|
459
|
-
self.environ['wsgi.input'],
|
460
|
-
self.content_length,
|
461
|
-
sha256,
|
462
|
-
hashed_payload)
|
463
|
-
# else, length not provided -- Swift will kick out a
|
464
|
-
# 411 Length Required which will get translated back
|
465
|
-
# to a S3-style response in S3Request._swift_error_codes
|
494
|
+
hashed_payload = self.headers.get('X-Amz-Content-SHA256',
|
495
|
+
'UNSIGNED-PAYLOAD')
|
496
|
+
|
466
497
|
cr.append(swob.wsgi_to_bytes(hashed_payload))
|
467
498
|
return b'\n'.join(cr)
|
468
499
|
|
@@ -524,18 +555,11 @@ class S3Request(swob.Request):
|
|
524
555
|
bucket_acl = _header_acl_property('container')
|
525
556
|
object_acl = _header_acl_property('object')
|
526
557
|
|
527
|
-
def __init__(self, env, app=None,
|
528
|
-
|
529
|
-
dns_compliant_bucket_names=True, allow_multipart_uploads=True,
|
530
|
-
allow_no_owner=False):
|
531
|
-
# NOTE: app and allow_no_owner are not used by this class, need for
|
532
|
-
# compatibility of S3acl
|
558
|
+
def __init__(self, env, app=None, conf=None):
|
559
|
+
# NOTE: app is not used by this class, need for compatibility of S3acl
|
533
560
|
swob.Request.__init__(self, env)
|
534
|
-
self.
|
535
|
-
self.location = location
|
536
|
-
self.force_request_log = force_request_log
|
537
|
-
self.dns_compliant_bucket_names = dns_compliant_bucket_names
|
538
|
-
self.allow_multipart_uploads = allow_multipart_uploads
|
561
|
+
self.conf = conf or Config()
|
562
|
+
self.location = self.conf.location
|
539
563
|
self._timestamp = None
|
540
564
|
self.access_key, self.signature = self._parse_auth_info()
|
541
565
|
self.bucket_in_host = self._parse_host()
|
@@ -551,20 +575,70 @@ class S3Request(swob.Request):
|
|
551
575
|
}
|
552
576
|
self.account = None
|
553
577
|
self.user_id = None
|
554
|
-
self.
|
578
|
+
self.policy_index = None
|
555
579
|
|
556
580
|
# Avoids that swift.swob.Response replaces Location header value
|
557
581
|
# by full URL when absolute path given. See swift.swob for more detail.
|
558
582
|
self.environ['swift.leave_relative_location'] = True
|
559
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
|
+
|
560
635
|
def check_signature(self, secret):
|
561
636
|
secret = utf8encode(secret)
|
562
637
|
user_signature = self.signature
|
563
638
|
valid_signature = base64.b64encode(hmac.new(
|
564
|
-
secret, self.string_to_sign, sha1
|
565
|
-
|
566
|
-
|
567
|
-
return user_signature == valid_signature
|
639
|
+
secret, self.string_to_sign, sha1
|
640
|
+
).digest()).strip().decode('ascii')
|
641
|
+
return streq_const_time(user_signature, valid_signature)
|
568
642
|
|
569
643
|
@property
|
570
644
|
def timestamp(self):
|
@@ -587,11 +661,13 @@ class S3Request(swob.Request):
|
|
587
661
|
self.headers.get('Date')))
|
588
662
|
except ValueError:
|
589
663
|
raise AccessDenied('AWS authentication requires a valid Date '
|
590
|
-
'or x-amz-date header'
|
664
|
+
'or x-amz-date header',
|
665
|
+
reason='invalid_date')
|
591
666
|
|
592
667
|
if timestamp < 0:
|
593
668
|
raise AccessDenied('AWS authentication requires a valid Date '
|
594
|
-
'or x-amz-date header'
|
669
|
+
'or x-amz-date header',
|
670
|
+
reason='invalid_date')
|
595
671
|
try:
|
596
672
|
self._timestamp = S3Timestamp(timestamp)
|
597
673
|
except ValueError:
|
@@ -609,29 +685,30 @@ class S3Request(swob.Request):
|
|
609
685
|
return 'AWSAccessKeyId' in self.params
|
610
686
|
|
611
687
|
def _parse_host(self):
|
612
|
-
|
613
|
-
if not storage_domain:
|
688
|
+
if not self.conf.storage_domains:
|
614
689
|
return None
|
615
690
|
|
616
|
-
if not storage_domain.startswith('.'):
|
617
|
-
storage_domain = '.' + storage_domain
|
618
|
-
|
619
691
|
if 'HTTP_HOST' in self.environ:
|
620
692
|
given_domain = self.environ['HTTP_HOST']
|
621
693
|
elif 'SERVER_NAME' in self.environ:
|
622
694
|
given_domain = self.environ['SERVER_NAME']
|
623
695
|
else:
|
624
696
|
return None
|
625
|
-
|
626
697
|
port = ''
|
627
698
|
if ':' in given_domain:
|
628
699
|
given_domain, port = given_domain.rsplit(':', 1)
|
629
|
-
|
630
|
-
|
700
|
+
|
701
|
+
for storage_domain in self.conf.storage_domains:
|
702
|
+
if not storage_domain.startswith('.'):
|
703
|
+
storage_domain = '.' + storage_domain
|
704
|
+
|
705
|
+
if given_domain.endswith(storage_domain):
|
706
|
+
return given_domain[:-len(storage_domain)]
|
631
707
|
|
632
708
|
return None
|
633
709
|
|
634
710
|
def _parse_uri(self):
|
711
|
+
# NB: returns WSGI strings
|
635
712
|
if not check_utf8(swob.wsgi_to_str(self.environ['PATH_INFO'])):
|
636
713
|
raise InvalidURI(self.path)
|
637
714
|
|
@@ -642,10 +719,10 @@ class S3Request(swob.Request):
|
|
642
719
|
bucket, obj = self.split_path(0, 2, True)
|
643
720
|
|
644
721
|
if bucket and not validate_bucket_name(
|
645
|
-
bucket, self.dns_compliant_bucket_names):
|
722
|
+
bucket, self.conf.dns_compliant_bucket_names):
|
646
723
|
# Ignore GET service case
|
647
724
|
raise InvalidBucketName(bucket)
|
648
|
-
return
|
725
|
+
return bucket, obj
|
649
726
|
|
650
727
|
def _parse_query_authentication(self):
|
651
728
|
"""
|
@@ -658,14 +735,14 @@ class S3Request(swob.Request):
|
|
658
735
|
:raises: AccessDenied
|
659
736
|
"""
|
660
737
|
try:
|
661
|
-
access = self.params['AWSAccessKeyId']
|
662
|
-
expires = self.params['Expires']
|
663
|
-
sig = self.params['Signature']
|
738
|
+
access = swob.wsgi_to_str(self.params['AWSAccessKeyId'])
|
739
|
+
expires = swob.wsgi_to_str(self.params['Expires'])
|
740
|
+
sig = swob.wsgi_to_str(self.params['Signature'])
|
664
741
|
except KeyError:
|
665
|
-
raise AccessDenied()
|
742
|
+
raise AccessDenied(reason='invalid_query_auth')
|
666
743
|
|
667
744
|
if not all([access, sig, expires]):
|
668
|
-
raise AccessDenied()
|
745
|
+
raise AccessDenied(reason='invalid_query_auth')
|
669
746
|
|
670
747
|
return access, sig
|
671
748
|
|
@@ -676,9 +753,9 @@ class S3Request(swob.Request):
|
|
676
753
|
:returns: a tuple of access_key and signature
|
677
754
|
:raises: AccessDenied
|
678
755
|
"""
|
679
|
-
auth_str = self.headers['Authorization']
|
756
|
+
auth_str = swob.wsgi_to_str(self.headers['Authorization'])
|
680
757
|
if not auth_str.startswith('AWS ') or ':' not in auth_str:
|
681
|
-
raise AccessDenied()
|
758
|
+
raise AccessDenied(reason='invalid_header_auth')
|
682
759
|
# This means signature format V2
|
683
760
|
access, sig = auth_str.split(' ', 1)[1].rsplit(':', 1)
|
684
761
|
return access, sig
|
@@ -709,15 +786,15 @@ class S3Request(swob.Request):
|
|
709
786
|
try:
|
710
787
|
ex = S3Timestamp(float(self.params['Expires']))
|
711
788
|
except (KeyError, ValueError):
|
712
|
-
raise AccessDenied()
|
789
|
+
raise AccessDenied(reason='invalid_expires')
|
713
790
|
|
714
791
|
if S3Timestamp.now() > ex:
|
715
|
-
raise AccessDenied('Request has expired')
|
792
|
+
raise AccessDenied('Request has expired', reason='expired')
|
716
793
|
|
717
794
|
if ex >= 2 ** 31:
|
718
795
|
raise AccessDenied(
|
719
796
|
'Invalid date (should be seconds since epoch): %s' %
|
720
|
-
self.params['Expires'])
|
797
|
+
self.params['Expires'], reason='invalid_expires')
|
721
798
|
|
722
799
|
def _validate_dates(self):
|
723
800
|
"""
|
@@ -729,19 +806,23 @@ class S3Request(swob.Request):
|
|
729
806
|
amz_date_header = self.headers.get('X-Amz-Date')
|
730
807
|
if not date_header and not amz_date_header:
|
731
808
|
raise AccessDenied('AWS authentication requires a valid Date '
|
732
|
-
'or x-amz-date header'
|
809
|
+
'or x-amz-date header',
|
810
|
+
reason='invalid_date')
|
733
811
|
|
734
812
|
# Anyways, request timestamp should be validated
|
735
813
|
epoch = S3Timestamp(0)
|
736
814
|
if self.timestamp < epoch:
|
737
|
-
raise AccessDenied()
|
815
|
+
raise AccessDenied(reason='invalid_date')
|
738
816
|
|
739
817
|
# If the standard date is too far ahead or behind, it is an
|
740
818
|
# error
|
741
|
-
delta =
|
742
|
-
if
|
819
|
+
delta = abs(int(self.timestamp) - int(S3Timestamp.now()))
|
820
|
+
if delta > self.conf.allowable_clock_skew:
|
743
821
|
raise RequestTimeTooSkewed()
|
744
822
|
|
823
|
+
def _validate_sha256(self):
|
824
|
+
return self.headers.get('x-amz-content-sha256')
|
825
|
+
|
745
826
|
def _validate_headers(self):
|
746
827
|
if 'CONTENT_LENGTH' in self.environ:
|
747
828
|
try:
|
@@ -752,21 +833,6 @@ class S3Request(swob.Request):
|
|
752
833
|
raise InvalidArgument('Content-Length',
|
753
834
|
self.environ['CONTENT_LENGTH'])
|
754
835
|
|
755
|
-
value = _header_strip(self.headers.get('Content-MD5'))
|
756
|
-
if value is not None:
|
757
|
-
if not re.match('^[A-Za-z0-9+/]+={0,2}$', value):
|
758
|
-
# Non-base64-alphabet characters in value.
|
759
|
-
raise InvalidDigest(content_md5=value)
|
760
|
-
try:
|
761
|
-
self.headers['ETag'] = binascii.b2a_hex(
|
762
|
-
binascii.a2b_base64(value))
|
763
|
-
except binascii.error:
|
764
|
-
# incorrect padding, most likely
|
765
|
-
raise InvalidDigest(content_md5=value)
|
766
|
-
|
767
|
-
if len(self.headers['ETag']) != 32:
|
768
|
-
raise InvalidDigest(content_md5=value)
|
769
|
-
|
770
836
|
if self.method == 'PUT' and any(h in self.headers for h in (
|
771
837
|
'If-Match', 'If-None-Match',
|
772
838
|
'If-Modified-Since', 'If-Unmodified-Since')):
|
@@ -782,7 +848,6 @@ class S3Request(swob.Request):
|
|
782
848
|
raise InvalidArgument('x-amz-copy-source',
|
783
849
|
self.headers['X-Amz-Copy-Source'],
|
784
850
|
msg)
|
785
|
-
|
786
851
|
if 'x-amz-metadata-directive' in self.headers:
|
787
852
|
value = self.headers['x-amz-metadata-directive']
|
788
853
|
if value not in ('COPY', 'REPLACE'):
|
@@ -813,6 +878,37 @@ class S3Request(swob.Request):
|
|
813
878
|
if 'x-amz-website-redirect-location' in self.headers:
|
814
879
|
raise S3NotImplemented('Website redirection is not supported.')
|
815
880
|
|
881
|
+
aws_sha256 = self._validate_sha256()
|
882
|
+
if (aws_sha256
|
883
|
+
and aws_sha256 != 'UNSIGNED-PAYLOAD'
|
884
|
+
and self.content_length is not None):
|
885
|
+
# Even if client-provided SHA doesn't look like a SHA, wrap the
|
886
|
+
# input anyway so we'll send the SHA of what the client sent in
|
887
|
+
# the eventual error
|
888
|
+
self.environ['wsgi.input'] = HashingInput(
|
889
|
+
self.environ['wsgi.input'],
|
890
|
+
self.content_length,
|
891
|
+
aws_sha256)
|
892
|
+
# If no content-length, either client's trying to do a HTTP chunked
|
893
|
+
# transfer, or a HTTP/1.0-style transfer (in which case swift will
|
894
|
+
# reject with length-required and we'll translate back to
|
895
|
+
# MissingContentLength)
|
896
|
+
|
897
|
+
value = _header_strip(self.headers.get('Content-MD5'))
|
898
|
+
if value is not None:
|
899
|
+
if not re.match('^[A-Za-z0-9+/]+={0,2}$', value):
|
900
|
+
# Non-base64-alphabet characters in value.
|
901
|
+
raise InvalidDigest(content_md5=value)
|
902
|
+
try:
|
903
|
+
self.headers['ETag'] = binascii.b2a_hex(
|
904
|
+
binascii.a2b_base64(value))
|
905
|
+
except binascii.Error:
|
906
|
+
# incorrect padding, most likely
|
907
|
+
raise InvalidDigest(content_md5=value)
|
908
|
+
|
909
|
+
if len(self.headers['ETag']) != 32:
|
910
|
+
raise InvalidDigest(content_md5=value)
|
911
|
+
|
816
912
|
# https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
|
817
913
|
# describes some of what would be required to support this
|
818
914
|
if any(['aws-chunked' in self.headers.get('content-encoding', ''),
|
@@ -852,7 +948,13 @@ class S3Request(swob.Request):
|
|
852
948
|
|
853
949
|
if te or ml:
|
854
950
|
# Limit the read similar to how SLO handles manifests
|
855
|
-
|
951
|
+
try:
|
952
|
+
body = self.body_file.read(max_length)
|
953
|
+
except S3InputSHA256Mismatch as err:
|
954
|
+
raise XAmzContentSHA256Mismatch(
|
955
|
+
client_computed_content_s_h_a256=err.expected,
|
956
|
+
s3_computed_content_s_h_a256=err.computed,
|
957
|
+
)
|
856
958
|
else:
|
857
959
|
# No (or zero) Content-Length provided, and not chunked transfer;
|
858
960
|
# no body. Assume zero-length, and enforce a required body below.
|
@@ -865,7 +967,8 @@ class S3Request(swob.Request):
|
|
865
967
|
raise InvalidRequest('Missing required header for this request: '
|
866
968
|
'Content-MD5')
|
867
969
|
|
868
|
-
digest = base64.b64encode(md5(
|
970
|
+
digest = base64.b64encode(md5(
|
971
|
+
body, usedforsecurity=False).digest()).strip().decode('ascii')
|
869
972
|
if self.environ['HTTP_CONTENT_MD5'] != digest:
|
870
973
|
raise BadDigest(content_md5=self.environ['HTTP_CONTENT_MD5'])
|
871
974
|
|
@@ -889,20 +992,16 @@ class S3Request(swob.Request):
|
|
889
992
|
except KeyError:
|
890
993
|
return None
|
891
994
|
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
# TODO: once we support versioning, we'll need to translate
|
903
|
-
# src_path to the proper location in the versions container
|
904
|
-
raise S3NotImplemented('Versioning is not yet supported')
|
905
|
-
self.headers['X-Amz-Copy-Source'] = src_path
|
995
|
+
src_path, qs = src_path.partition('?')[::2]
|
996
|
+
parsed = parse_qsl(qs, True)
|
997
|
+
if not parsed:
|
998
|
+
query = {}
|
999
|
+
elif len(parsed) == 1 and parsed[0][0] == 'versionId':
|
1000
|
+
query = {'version-id': parsed[0][1]}
|
1001
|
+
else:
|
1002
|
+
raise InvalidArgument('X-Amz-Copy-Source',
|
1003
|
+
self.headers['X-Amz-Copy-Source'],
|
1004
|
+
'Unsupported copy source parameter.')
|
906
1005
|
|
907
1006
|
src_path = unquote(src_path)
|
908
1007
|
src_path = src_path if src_path.startswith('/') else ('/' + src_path)
|
@@ -911,20 +1010,17 @@ class S3Request(swob.Request):
|
|
911
1010
|
headers = swob.HeaderKeyDict()
|
912
1011
|
headers.update(self._copy_source_headers())
|
913
1012
|
|
914
|
-
src_resp = self.get_response(app, 'HEAD', src_bucket,
|
915
|
-
|
1013
|
+
src_resp = self.get_response(app, 'HEAD', src_bucket,
|
1014
|
+
swob.str_to_wsgi(src_obj),
|
1015
|
+
headers=headers, query=query)
|
916
1016
|
if src_resp.status_int == 304: # pylint: disable-msg=E1101
|
917
1017
|
raise PreconditionFailed()
|
918
1018
|
|
919
|
-
self.
|
920
|
-
|
921
|
-
source_container, source_obj = \
|
922
|
-
split_path(self.headers['X-Amz-Copy-Source'], 1, 2, True)
|
923
|
-
|
924
|
-
if (self.container_name == source_container and
|
925
|
-
self.object_name == source_obj and
|
1019
|
+
if (self.container_name == src_bucket and
|
1020
|
+
self.object_name == src_obj and
|
926
1021
|
self.headers.get('x-amz-metadata-directive',
|
927
|
-
'COPY') == 'COPY'
|
1022
|
+
'COPY') == 'COPY' and
|
1023
|
+
not query):
|
928
1024
|
raise InvalidRequest("This copy request is illegal "
|
929
1025
|
"because it is trying to copy an "
|
930
1026
|
"object to itself without "
|
@@ -932,6 +1028,12 @@ class S3Request(swob.Request):
|
|
932
1028
|
"storage class, website redirect "
|
933
1029
|
"location or encryption "
|
934
1030
|
"attributes.")
|
1031
|
+
# We've done some normalizing; write back so it's ready for
|
1032
|
+
# to_swift_req
|
1033
|
+
self.headers['X-Amz-Copy-Source'] = quote(src_path)
|
1034
|
+
if query:
|
1035
|
+
self.headers['X-Amz-Copy-Source'] += \
|
1036
|
+
'?versionId=' + query['version-id']
|
935
1037
|
return src_resp
|
936
1038
|
|
937
1039
|
def _canonical_uri(self):
|
@@ -979,7 +1081,7 @@ class S3Request(swob.Request):
|
|
979
1081
|
else:
|
980
1082
|
# Should have already raised NotS3Request in _parse_auth_info,
|
981
1083
|
# but as a sanity check...
|
982
|
-
raise AccessDenied()
|
1084
|
+
raise AccessDenied(reason='not_s3')
|
983
1085
|
|
984
1086
|
for key, value in sorted(amz_headers.items()):
|
985
1087
|
buf.append(swob.wsgi_to_bytes("%s:%s" % (key, value)))
|
@@ -1018,7 +1120,7 @@ class S3Request(swob.Request):
|
|
1018
1120
|
if self.is_service_request:
|
1019
1121
|
return ServiceController
|
1020
1122
|
|
1021
|
-
if not self.
|
1123
|
+
if not self.conf.allow_multipart_uploads:
|
1022
1124
|
multi_part = ['partNumber', 'uploadId', 'uploads']
|
1023
1125
|
if len([p for p in multi_part if p in self.params]):
|
1024
1126
|
raise S3NotImplemented("Multi-part feature isn't support")
|
@@ -1032,16 +1134,23 @@ class S3Request(swob.Request):
|
|
1032
1134
|
if 'logging' in self.params:
|
1033
1135
|
return LoggingStatusController
|
1034
1136
|
if 'partNumber' in self.params:
|
1035
|
-
|
1137
|
+
if self.method == 'PUT':
|
1138
|
+
return PartController
|
1139
|
+
else:
|
1140
|
+
return ObjectController
|
1036
1141
|
if 'uploadId' in self.params:
|
1037
1142
|
return UploadController
|
1038
1143
|
if 'uploads' in self.params:
|
1039
1144
|
return UploadsController
|
1040
1145
|
if 'versioning' in self.params:
|
1041
1146
|
return VersioningController
|
1147
|
+
if 'tagging' in self.params:
|
1148
|
+
return TaggingController
|
1149
|
+
if 'object-lock' in self.params:
|
1150
|
+
return ObjectLockController
|
1042
1151
|
|
1043
1152
|
unsupported = ('notification', 'policy', 'requestPayment', 'torrent',
|
1044
|
-
'website', 'cors', '
|
1153
|
+
'website', 'cors', 'restore')
|
1045
1154
|
if set(unsupported) & set(self.params):
|
1046
1155
|
return UnsupportedController
|
1047
1156
|
|
@@ -1071,11 +1180,12 @@ class S3Request(swob.Request):
|
|
1071
1180
|
Create a Swift request based on this request's environment.
|
1072
1181
|
"""
|
1073
1182
|
if self.account is None:
|
1074
|
-
account = self.access_key
|
1183
|
+
account = swob.str_to_wsgi(self.access_key)
|
1075
1184
|
else:
|
1076
1185
|
account = self.account
|
1077
1186
|
|
1078
1187
|
env = self.environ.copy()
|
1188
|
+
env['swift.infocache'] = self.environ.setdefault('swift.infocache', {})
|
1079
1189
|
|
1080
1190
|
def sanitize(value):
|
1081
1191
|
if set(value).issubset(string.printable):
|
@@ -1121,8 +1231,10 @@ class S3Request(swob.Request):
|
|
1121
1231
|
env['HTTP_X_OBJECT_META_' + key[16:]] = sanitize(env[key])
|
1122
1232
|
del env[key]
|
1123
1233
|
|
1124
|
-
|
1125
|
-
|
1234
|
+
copy_from_version_id = ''
|
1235
|
+
if 'HTTP_X_AMZ_COPY_SOURCE' in env and env['REQUEST_METHOD'] == 'PUT':
|
1236
|
+
env['HTTP_X_COPY_FROM'], copy_from_version_id = env[
|
1237
|
+
'HTTP_X_AMZ_COPY_SOURCE'].partition('?versionId=')[::2]
|
1126
1238
|
del env['HTTP_X_AMZ_COPY_SOURCE']
|
1127
1239
|
env['CONTENT_LENGTH'] = '0'
|
1128
1240
|
if env.pop('HTTP_X_AMZ_METADATA_DIRECTIVE', None) == 'REPLACE':
|
@@ -1141,7 +1253,7 @@ class S3Request(swob.Request):
|
|
1141
1253
|
if key.startswith('HTTP_X_OBJECT_META_'):
|
1142
1254
|
del env[key]
|
1143
1255
|
|
1144
|
-
if self.
|
1256
|
+
if self.conf.force_swift_request_proxy_log:
|
1145
1257
|
env['swift.proxy_access_log_made'] = False
|
1146
1258
|
env['swift.source'] = 'S3'
|
1147
1259
|
if method is not None:
|
@@ -1155,16 +1267,16 @@ class S3Request(swob.Request):
|
|
1155
1267
|
path = '/v1/%s' % (account)
|
1156
1268
|
env['PATH_INFO'] = path
|
1157
1269
|
|
1158
|
-
|
1270
|
+
params = []
|
1159
1271
|
if query is not None:
|
1160
|
-
params = []
|
1161
1272
|
for key, value in sorted(query.items()):
|
1162
1273
|
if value is not None:
|
1163
1274
|
params.append('%s=%s' % (key, quote(str(value))))
|
1164
1275
|
else:
|
1165
1276
|
params.append(key)
|
1166
|
-
|
1167
|
-
|
1277
|
+
if copy_from_version_id and not (query and query.get('version-id')):
|
1278
|
+
params.append('version-id=' + copy_from_version_id)
|
1279
|
+
env['QUERY_STRING'] = '&'.join(params)
|
1168
1280
|
|
1169
1281
|
return swob.Request.blank(quote(path), environ=env, body=body,
|
1170
1282
|
headers=headers)
|
@@ -1230,7 +1342,7 @@ class S3Request(swob.Request):
|
|
1230
1342
|
|
1231
1343
|
def _bucket_put_accepted_error(self, container, app):
|
1232
1344
|
sw_req = self.to_swift_req('HEAD', container, None)
|
1233
|
-
info = get_container_info(sw_req.environ, app)
|
1345
|
+
info = get_container_info(sw_req.environ, app, swift_source='S3')
|
1234
1346
|
sysmeta = info.get('sysmeta', {})
|
1235
1347
|
try:
|
1236
1348
|
acl = json.loads(sysmeta.get('s3api-acl',
|
@@ -1288,6 +1400,16 @@ class S3Request(swob.Request):
|
|
1288
1400
|
return NoSuchKey(obj)
|
1289
1401
|
return NoSuchBucket(container)
|
1290
1402
|
|
1403
|
+
# Since BadDigest ought to plumb in some client-provided values,
|
1404
|
+
# defer evaluation until we know they're provided
|
1405
|
+
def bad_digest_handler():
|
1406
|
+
etag = binascii.hexlify(base64.b64decode(
|
1407
|
+
env['HTTP_CONTENT_MD5']))
|
1408
|
+
return BadDigest(
|
1409
|
+
expected_digest=etag, # yes, really hex
|
1410
|
+
# TODO: plumb in calculated_digest, as b64
|
1411
|
+
)
|
1412
|
+
|
1291
1413
|
code_map = {
|
1292
1414
|
'HEAD': {
|
1293
1415
|
HTTP_NOT_FOUND: not_found_handler,
|
@@ -1296,14 +1418,15 @@ class S3Request(swob.Request):
|
|
1296
1418
|
'GET': {
|
1297
1419
|
HTTP_NOT_FOUND: not_found_handler,
|
1298
1420
|
HTTP_PRECONDITION_FAILED: PreconditionFailed,
|
1299
|
-
HTTP_REQUESTED_RANGE_NOT_SATISFIABLE: InvalidRange,
|
1300
1421
|
},
|
1301
1422
|
'PUT': {
|
1302
1423
|
HTTP_NOT_FOUND: (NoSuchBucket, container),
|
1303
|
-
HTTP_UNPROCESSABLE_ENTITY:
|
1424
|
+
HTTP_UNPROCESSABLE_ENTITY: bad_digest_handler,
|
1304
1425
|
HTTP_REQUEST_ENTITY_TOO_LARGE: EntityTooLarge,
|
1305
1426
|
HTTP_LENGTH_REQUIRED: MissingContentLength,
|
1306
1427
|
HTTP_REQUEST_TIMEOUT: RequestTimeout,
|
1428
|
+
HTTP_PRECONDITION_FAILED: PreconditionFailed,
|
1429
|
+
HTTP_CLIENT_CLOSED_REQUEST: RequestTimeout,
|
1307
1430
|
},
|
1308
1431
|
'POST': {
|
1309
1432
|
HTTP_NOT_FOUND: not_found_handler,
|
@@ -1335,13 +1458,25 @@ class S3Request(swob.Request):
|
|
1335
1458
|
|
1336
1459
|
try:
|
1337
1460
|
sw_resp = sw_req.get_response(app)
|
1338
|
-
except
|
1339
|
-
|
1461
|
+
except S3InputSHA256Mismatch as err:
|
1462
|
+
# hopefully by now any modifications to the path (e.g. tenant to
|
1463
|
+
# account translation) will have been made by auth middleware
|
1464
|
+
self.environ['s3api.backend_path'] = sw_req.environ['PATH_INFO']
|
1465
|
+
raise XAmzContentSHA256Mismatch(
|
1466
|
+
client_computed_content_s_h_a256=err.expected,
|
1467
|
+
s3_computed_content_s_h_a256=err.computed,
|
1468
|
+
)
|
1340
1469
|
else:
|
1341
1470
|
# reuse account
|
1342
1471
|
_, self.account, _ = split_path(sw_resp.environ['PATH_INFO'],
|
1343
1472
|
2, 3, True)
|
1473
|
+
# Update s3.backend_path from the response environ
|
1474
|
+
self.environ['s3api.backend_path'] = sw_resp.environ['PATH_INFO']
|
1344
1475
|
|
1476
|
+
# keep a record of the backend policy index so that the s3api can add
|
1477
|
+
# it to the headers of whatever response it returns, which may not
|
1478
|
+
# necessarily be this resp.
|
1479
|
+
self.policy_index = get_policy_index(sw_req.headers, sw_resp.headers)
|
1345
1480
|
resp = S3Response.from_swift_resp(sw_resp)
|
1346
1481
|
status = resp.status_int # pylint: disable-msg=E1101
|
1347
1482
|
|
@@ -1351,8 +1486,6 @@ class S3Request(swob.Request):
|
|
1351
1486
|
self.user_id = "%s:%s" % (
|
1352
1487
|
sw_resp.environ['HTTP_X_TENANT_NAME'],
|
1353
1488
|
sw_resp.environ['HTTP_X_USER_NAME'])
|
1354
|
-
if six.PY2 and not isinstance(self.user_id, bytes):
|
1355
|
-
self.user_id = self.user_id.encode('utf8')
|
1356
1489
|
else:
|
1357
1490
|
# tempauth
|
1358
1491
|
self.user_id = self.access_key
|
@@ -1371,20 +1504,43 @@ class S3Request(swob.Request):
|
|
1371
1504
|
error_codes[sw_resp.status_int] # pylint: disable-msg=E1101
|
1372
1505
|
if isinstance(err_resp, tuple):
|
1373
1506
|
raise err_resp[0](*err_resp[1:])
|
1507
|
+
elif b'quota' in err_msg:
|
1508
|
+
raise err_resp(err_msg)
|
1374
1509
|
else:
|
1375
1510
|
raise err_resp()
|
1376
1511
|
|
1377
1512
|
if status == HTTP_BAD_REQUEST:
|
1378
|
-
|
1513
|
+
err_str = err_msg.decode('utf8')
|
1514
|
+
if 'X-Delete-At' in err_str:
|
1515
|
+
raise InvalidArgument('X-Delete-At',
|
1516
|
+
self.headers['X-Delete-At'],
|
1517
|
+
err_str)
|
1518
|
+
if 'X-Delete-After' in err_str:
|
1519
|
+
raise InvalidArgument('X-Delete-After',
|
1520
|
+
self.headers['X-Delete-After'],
|
1521
|
+
err_str)
|
1522
|
+
else:
|
1523
|
+
raise InvalidRequest(msg=err_str)
|
1379
1524
|
if status == HTTP_UNAUTHORIZED:
|
1380
1525
|
raise SignatureDoesNotMatch(
|
1381
1526
|
**self.signature_does_not_match_kwargs())
|
1382
1527
|
if status == HTTP_FORBIDDEN:
|
1383
|
-
raise AccessDenied()
|
1528
|
+
raise AccessDenied(reason='forbidden')
|
1529
|
+
if status == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
|
1530
|
+
self.validate_part_number(
|
1531
|
+
parts_count=resp.headers.get('x-amz-mp-parts-count'))
|
1532
|
+
raise InvalidRange()
|
1384
1533
|
if status == HTTP_SERVICE_UNAVAILABLE:
|
1385
1534
|
raise ServiceUnavailable()
|
1386
1535
|
if status in (HTTP_RATE_LIMITED, HTTP_TOO_MANY_REQUESTS):
|
1536
|
+
if self.conf.ratelimit_as_client_error:
|
1537
|
+
raise SlowDown(status='429 Slow Down')
|
1387
1538
|
raise SlowDown()
|
1539
|
+
if resp.status_int == HTTP_CONFLICT:
|
1540
|
+
if self.method == 'GET':
|
1541
|
+
raise BrokenMPU()
|
1542
|
+
else:
|
1543
|
+
raise ServiceUnavailable()
|
1388
1544
|
|
1389
1545
|
raise InternalError('unexpected status code %d' % status)
|
1390
1546
|
|
@@ -1439,34 +1595,49 @@ class S3Request(swob.Request):
|
|
1439
1595
|
:raises: NoSuchBucket when the container doesn't exist
|
1440
1596
|
:raises: InternalError when the request failed without 404
|
1441
1597
|
"""
|
1442
|
-
if self.is_authenticated:
|
1443
|
-
|
1444
|
-
#
|
1445
|
-
sw_req =
|
1446
|
-
|
1447
|
-
|
1448
|
-
|
1449
|
-
|
1450
|
-
raise
|
1451
|
-
|
1452
|
-
|
1453
|
-
|
1598
|
+
if not self.is_authenticated:
|
1599
|
+
sw_req = self.to_swift_req('TEST', None, None, body='')
|
1600
|
+
# don't show log message of this request
|
1601
|
+
sw_req.environ['swift.proxy_access_log_made'] = True
|
1602
|
+
|
1603
|
+
sw_resp = sw_req.get_response(app)
|
1604
|
+
|
1605
|
+
if not sw_req.remote_user:
|
1606
|
+
raise SignatureDoesNotMatch(
|
1607
|
+
**self.signature_does_not_match_kwargs())
|
1608
|
+
|
1609
|
+
_, self.account, _ = split_path(sw_resp.environ['PATH_INFO'],
|
1610
|
+
2, 3, True)
|
1611
|
+
sw_req = self.to_swift_req('TEST', self.container_name, None)
|
1612
|
+
info = get_container_info(sw_req.environ, app, swift_source='S3')
|
1613
|
+
if is_success(info['status']):
|
1614
|
+
return info
|
1615
|
+
elif info['status'] == HTTP_NOT_FOUND:
|
1616
|
+
raise NoSuchBucket(self.container_name)
|
1617
|
+
elif info['status'] == HTTP_SERVICE_UNAVAILABLE:
|
1618
|
+
raise ServiceUnavailable()
|
1454
1619
|
else:
|
1455
|
-
|
1456
|
-
|
1457
|
-
|
1458
|
-
|
1459
|
-
|
1460
|
-
|
1461
|
-
|
1462
|
-
def gen_multipart_manifest_delete_query(self, app, obj=None):
|
1463
|
-
if not self.allow_multipart_uploads:
|
1464
|
-
return None
|
1465
|
-
query = {'multipart-manifest': 'delete'}
|
1620
|
+
raise InternalError(
|
1621
|
+
'unexpected status code %d' % info['status'])
|
1622
|
+
|
1623
|
+
def gen_multipart_manifest_delete_query(self, app, obj=None, version=None):
|
1624
|
+
if not self.conf.allow_multipart_uploads:
|
1625
|
+
return {}
|
1466
1626
|
if not obj:
|
1467
1627
|
obj = self.object_name
|
1468
|
-
|
1469
|
-
|
1628
|
+
query = {'symlink': 'get'}
|
1629
|
+
if version is not None:
|
1630
|
+
query['version-id'] = version
|
1631
|
+
resp = self.get_response(app, 'HEAD', obj=obj, query=query)
|
1632
|
+
if not resp.is_slo:
|
1633
|
+
return {}
|
1634
|
+
elif resp.sysmeta_headers.get(sysmeta_header('object', 'etag')):
|
1635
|
+
# Even if allow_async_delete is turned off, SLO will just handle
|
1636
|
+
# the delete synchronously, so we don't need to check before
|
1637
|
+
# setting async=on
|
1638
|
+
return {'multipart-manifest': 'delete', 'async': 'on'}
|
1639
|
+
else:
|
1640
|
+
return {'multipart-manifest': 'delete'}
|
1470
1641
|
|
1471
1642
|
def set_acl_handler(self, handler):
|
1472
1643
|
pass
|
@@ -1476,14 +1647,9 @@ class S3AclRequest(S3Request):
|
|
1476
1647
|
"""
|
1477
1648
|
S3Acl request object.
|
1478
1649
|
"""
|
1479
|
-
|
1480
|
-
|
1481
|
-
|
1482
|
-
allow_no_owner=False):
|
1483
|
-
super(S3AclRequest, self).__init__(
|
1484
|
-
env, app, slo_enabled, storage_domain, location, force_request_log,
|
1485
|
-
dns_compliant_bucket_names, allow_multipart_uploads)
|
1486
|
-
self.allow_no_owner = allow_no_owner
|
1650
|
+
|
1651
|
+
def __init__(self, env, app=None, conf=None):
|
1652
|
+
super(S3AclRequest, self).__init__(env, app, conf)
|
1487
1653
|
self.authenticate(app)
|
1488
1654
|
self.acl_handler = None
|
1489
1655
|
|
@@ -1517,14 +1683,14 @@ class S3AclRequest(S3Request):
|
|
1517
1683
|
# keystone
|
1518
1684
|
self.user_id = "%s:%s" % (sw_resp.environ['HTTP_X_TENANT_NAME'],
|
1519
1685
|
sw_resp.environ['HTTP_X_USER_NAME'])
|
1520
|
-
if six.PY2 and not isinstance(self.user_id, bytes):
|
1521
|
-
self.user_id = self.user_id.encode('utf8')
|
1522
1686
|
else:
|
1523
1687
|
# tempauth
|
1524
1688
|
self.user_id = self.access_key
|
1525
1689
|
|
1526
1690
|
sw_req.environ.get('swift.authorize', lambda req: None)(sw_req)
|
1527
1691
|
self.environ['swift_owner'] = sw_req.environ.get('swift_owner', False)
|
1692
|
+
if 'REMOTE_USER' in sw_req.environ:
|
1693
|
+
self.environ['REMOTE_USER'] = sw_req.environ['REMOTE_USER']
|
1528
1694
|
|
1529
1695
|
# Need to skip S3 authorization on subsequent requests to prevent
|
1530
1696
|
# overwriting the account in PATH_INFO
|
@@ -1550,9 +1716,9 @@ class S3AclRequest(S3Request):
|
|
1550
1716
|
resp = self._get_response(
|
1551
1717
|
app, method, container, obj, headers, body, query)
|
1552
1718
|
resp.bucket_acl = decode_acl(
|
1553
|
-
'container', resp.sysmeta_headers, self.allow_no_owner)
|
1719
|
+
'container', resp.sysmeta_headers, self.conf.allow_no_owner)
|
1554
1720
|
resp.object_acl = decode_acl(
|
1555
|
-
'object', resp.sysmeta_headers, self.allow_no_owner)
|
1721
|
+
'object', resp.sysmeta_headers, self.conf.allow_no_owner)
|
1556
1722
|
|
1557
1723
|
return resp
|
1558
1724
|
|