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.
Files changed (208) hide show
  1. swift/__init__.py +29 -50
  2. swift/account/auditor.py +21 -118
  3. swift/account/backend.py +33 -28
  4. swift/account/reaper.py +37 -28
  5. swift/account/replicator.py +22 -0
  6. swift/account/server.py +60 -26
  7. swift/account/utils.py +28 -11
  8. swift-2.23.2.data/scripts/swift-account-audit → swift/cli/account_audit.py +23 -13
  9. swift-2.23.2.data/scripts/swift-config → swift/cli/config.py +2 -2
  10. swift/cli/container_deleter.py +5 -11
  11. swift-2.23.2.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +8 -7
  12. swift/cli/dispersion_report.py +10 -9
  13. swift-2.23.2.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +63 -21
  14. swift/cli/form_signature.py +3 -7
  15. swift-2.23.2.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +8 -2
  16. swift/cli/info.py +183 -29
  17. swift/cli/manage_shard_ranges.py +708 -37
  18. swift-2.23.2.data/scripts/swift-oldies → swift/cli/oldies.py +25 -14
  19. swift-2.23.2.data/scripts/swift-orphans → swift/cli/orphans.py +7 -3
  20. swift/cli/recon.py +196 -67
  21. swift-2.23.2.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +17 -20
  22. swift-2.23.2.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
  23. swift/cli/relinker.py +807 -126
  24. swift/cli/reload.py +135 -0
  25. swift/cli/ringbuilder.py +217 -20
  26. swift/cli/ringcomposer.py +0 -1
  27. swift/cli/shard-info.py +4 -3
  28. swift/common/base_storage_server.py +9 -20
  29. swift/common/bufferedhttp.py +48 -74
  30. swift/common/constraints.py +20 -15
  31. swift/common/container_sync_realms.py +9 -11
  32. swift/common/daemon.py +25 -8
  33. swift/common/db.py +198 -127
  34. swift/common/db_auditor.py +168 -0
  35. swift/common/db_replicator.py +95 -55
  36. swift/common/digest.py +141 -0
  37. swift/common/direct_client.py +144 -33
  38. swift/common/error_limiter.py +93 -0
  39. swift/common/exceptions.py +25 -1
  40. swift/common/header_key_dict.py +2 -9
  41. swift/common/http_protocol.py +373 -0
  42. swift/common/internal_client.py +129 -59
  43. swift/common/linkat.py +3 -4
  44. swift/common/manager.py +284 -67
  45. swift/common/memcached.py +396 -147
  46. swift/common/middleware/__init__.py +4 -0
  47. swift/common/middleware/account_quotas.py +211 -46
  48. swift/common/middleware/acl.py +3 -8
  49. swift/common/middleware/backend_ratelimit.py +230 -0
  50. swift/common/middleware/bulk.py +22 -34
  51. swift/common/middleware/catch_errors.py +1 -3
  52. swift/common/middleware/cname_lookup.py +6 -11
  53. swift/common/middleware/container_quotas.py +1 -1
  54. swift/common/middleware/container_sync.py +39 -17
  55. swift/common/middleware/copy.py +12 -0
  56. swift/common/middleware/crossdomain.py +22 -9
  57. swift/common/middleware/crypto/__init__.py +2 -1
  58. swift/common/middleware/crypto/crypto_utils.py +11 -15
  59. swift/common/middleware/crypto/decrypter.py +28 -11
  60. swift/common/middleware/crypto/encrypter.py +12 -17
  61. swift/common/middleware/crypto/keymaster.py +8 -15
  62. swift/common/middleware/crypto/kms_keymaster.py +2 -1
  63. swift/common/middleware/dlo.py +15 -11
  64. swift/common/middleware/domain_remap.py +5 -4
  65. swift/common/middleware/etag_quoter.py +128 -0
  66. swift/common/middleware/formpost.py +73 -70
  67. swift/common/middleware/gatekeeper.py +8 -1
  68. swift/common/middleware/keystoneauth.py +33 -3
  69. swift/common/middleware/list_endpoints.py +4 -4
  70. swift/common/middleware/listing_formats.py +85 -49
  71. swift/common/middleware/memcache.py +4 -81
  72. swift/common/middleware/name_check.py +3 -2
  73. swift/common/middleware/proxy_logging.py +160 -92
  74. swift/common/middleware/ratelimit.py +17 -10
  75. swift/common/middleware/read_only.py +6 -4
  76. swift/common/middleware/recon.py +59 -22
  77. swift/common/middleware/s3api/acl_handlers.py +25 -3
  78. swift/common/middleware/s3api/acl_utils.py +6 -1
  79. swift/common/middleware/s3api/controllers/__init__.py +6 -0
  80. swift/common/middleware/s3api/controllers/acl.py +3 -2
  81. swift/common/middleware/s3api/controllers/bucket.py +242 -137
  82. swift/common/middleware/s3api/controllers/logging.py +2 -2
  83. swift/common/middleware/s3api/controllers/multi_delete.py +43 -20
  84. swift/common/middleware/s3api/controllers/multi_upload.py +219 -133
  85. swift/common/middleware/s3api/controllers/obj.py +112 -8
  86. swift/common/middleware/s3api/controllers/object_lock.py +44 -0
  87. swift/common/middleware/s3api/controllers/s3_acl.py +2 -2
  88. swift/common/middleware/s3api/controllers/tagging.py +57 -0
  89. swift/common/middleware/s3api/controllers/versioning.py +36 -7
  90. swift/common/middleware/s3api/etree.py +22 -9
  91. swift/common/middleware/s3api/exception.py +0 -4
  92. swift/common/middleware/s3api/s3api.py +113 -41
  93. swift/common/middleware/s3api/s3request.py +384 -218
  94. swift/common/middleware/s3api/s3response.py +126 -23
  95. swift/common/middleware/s3api/s3token.py +16 -17
  96. swift/common/middleware/s3api/schema/delete.rng +1 -1
  97. swift/common/middleware/s3api/subresource.py +7 -10
  98. swift/common/middleware/s3api/utils.py +27 -10
  99. swift/common/middleware/slo.py +665 -358
  100. swift/common/middleware/staticweb.py +64 -37
  101. swift/common/middleware/symlink.py +52 -19
  102. swift/common/middleware/tempauth.py +76 -58
  103. swift/common/middleware/tempurl.py +192 -174
  104. swift/common/middleware/versioned_writes/__init__.py +51 -0
  105. swift/common/middleware/{versioned_writes.py → versioned_writes/legacy.py} +27 -26
  106. swift/common/middleware/versioned_writes/object_versioning.py +1482 -0
  107. swift/common/middleware/x_profile/exceptions.py +1 -4
  108. swift/common/middleware/x_profile/html_viewer.py +18 -19
  109. swift/common/middleware/x_profile/profile_model.py +1 -2
  110. swift/common/middleware/xprofile.py +10 -10
  111. swift-2.23.2.data/scripts/swift-container-server → swift/common/recon.py +13 -8
  112. swift/common/registry.py +147 -0
  113. swift/common/request_helpers.py +324 -57
  114. swift/common/ring/builder.py +67 -25
  115. swift/common/ring/composite_builder.py +1 -1
  116. swift/common/ring/ring.py +177 -51
  117. swift/common/ring/utils.py +1 -1
  118. swift/common/splice.py +10 -6
  119. swift/common/statsd_client.py +205 -0
  120. swift/common/storage_policy.py +49 -44
  121. swift/common/swob.py +86 -102
  122. swift/common/{utils.py → utils/__init__.py} +2191 -2762
  123. swift/common/utils/base.py +131 -0
  124. swift/common/utils/config.py +433 -0
  125. swift/common/utils/ipaddrs.py +256 -0
  126. swift/common/utils/libc.py +345 -0
  127. swift/common/utils/logs.py +859 -0
  128. swift/common/utils/timestamp.py +412 -0
  129. swift/common/wsgi.py +555 -536
  130. swift/container/auditor.py +14 -100
  131. swift/container/backend.py +552 -227
  132. swift/container/reconciler.py +126 -37
  133. swift/container/replicator.py +96 -22
  134. swift/container/server.py +397 -176
  135. swift/container/sharder.py +1580 -639
  136. swift/container/sync.py +94 -88
  137. swift/container/updater.py +53 -32
  138. swift/obj/auditor.py +153 -35
  139. swift/obj/diskfile.py +466 -217
  140. swift/obj/expirer.py +406 -124
  141. swift/obj/mem_diskfile.py +7 -4
  142. swift/obj/mem_server.py +1 -0
  143. swift/obj/reconstructor.py +523 -262
  144. swift/obj/replicator.py +249 -188
  145. swift/obj/server.py +213 -122
  146. swift/obj/ssync_receiver.py +145 -85
  147. swift/obj/ssync_sender.py +113 -54
  148. swift/obj/updater.py +653 -139
  149. swift/obj/watchers/__init__.py +0 -0
  150. swift/obj/watchers/dark_data.py +213 -0
  151. swift/proxy/controllers/account.py +11 -11
  152. swift/proxy/controllers/base.py +848 -604
  153. swift/proxy/controllers/container.py +452 -86
  154. swift/proxy/controllers/info.py +3 -2
  155. swift/proxy/controllers/obj.py +1009 -490
  156. swift/proxy/server.py +185 -112
  157. swift-2.35.0.dist-info/AUTHORS +501 -0
  158. swift-2.35.0.dist-info/LICENSE +202 -0
  159. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/METADATA +52 -61
  160. swift-2.35.0.dist-info/RECORD +201 -0
  161. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/WHEEL +1 -1
  162. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/entry_points.txt +43 -0
  163. swift-2.35.0.dist-info/pbr.json +1 -0
  164. swift/locale/de/LC_MESSAGES/swift.po +0 -1216
  165. swift/locale/en_GB/LC_MESSAGES/swift.po +0 -1207
  166. swift/locale/es/LC_MESSAGES/swift.po +0 -1085
  167. swift/locale/fr/LC_MESSAGES/swift.po +0 -909
  168. swift/locale/it/LC_MESSAGES/swift.po +0 -894
  169. swift/locale/ja/LC_MESSAGES/swift.po +0 -965
  170. swift/locale/ko_KR/LC_MESSAGES/swift.po +0 -964
  171. swift/locale/pt_BR/LC_MESSAGES/swift.po +0 -881
  172. swift/locale/ru/LC_MESSAGES/swift.po +0 -891
  173. swift/locale/tr_TR/LC_MESSAGES/swift.po +0 -832
  174. swift/locale/zh_CN/LC_MESSAGES/swift.po +0 -833
  175. swift/locale/zh_TW/LC_MESSAGES/swift.po +0 -838
  176. swift-2.23.2.data/scripts/swift-account-auditor +0 -23
  177. swift-2.23.2.data/scripts/swift-account-info +0 -51
  178. swift-2.23.2.data/scripts/swift-account-reaper +0 -23
  179. swift-2.23.2.data/scripts/swift-account-replicator +0 -34
  180. swift-2.23.2.data/scripts/swift-account-server +0 -23
  181. swift-2.23.2.data/scripts/swift-container-auditor +0 -23
  182. swift-2.23.2.data/scripts/swift-container-info +0 -51
  183. swift-2.23.2.data/scripts/swift-container-reconciler +0 -21
  184. swift-2.23.2.data/scripts/swift-container-replicator +0 -34
  185. swift-2.23.2.data/scripts/swift-container-sharder +0 -33
  186. swift-2.23.2.data/scripts/swift-container-sync +0 -23
  187. swift-2.23.2.data/scripts/swift-container-updater +0 -23
  188. swift-2.23.2.data/scripts/swift-dispersion-report +0 -24
  189. swift-2.23.2.data/scripts/swift-form-signature +0 -20
  190. swift-2.23.2.data/scripts/swift-init +0 -119
  191. swift-2.23.2.data/scripts/swift-object-auditor +0 -29
  192. swift-2.23.2.data/scripts/swift-object-expirer +0 -33
  193. swift-2.23.2.data/scripts/swift-object-info +0 -60
  194. swift-2.23.2.data/scripts/swift-object-reconstructor +0 -33
  195. swift-2.23.2.data/scripts/swift-object-relinker +0 -41
  196. swift-2.23.2.data/scripts/swift-object-replicator +0 -37
  197. swift-2.23.2.data/scripts/swift-object-server +0 -27
  198. swift-2.23.2.data/scripts/swift-object-updater +0 -23
  199. swift-2.23.2.data/scripts/swift-proxy-server +0 -23
  200. swift-2.23.2.data/scripts/swift-recon +0 -24
  201. swift-2.23.2.data/scripts/swift-ring-builder +0 -24
  202. swift-2.23.2.data/scripts/swift-ring-builder-analyzer +0 -22
  203. swift-2.23.2.data/scripts/swift-ring-composer +0 -22
  204. swift-2.23.2.dist-info/DESCRIPTION.rst +0 -166
  205. swift-2.23.2.dist-info/RECORD +0 -220
  206. swift-2.23.2.dist-info/metadata.json +0 -1
  207. swift-2.23.2.dist-info/pbr.json +0 -1
  208. {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, md5
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 six.moves.urllib.parse import quote, unquote, parse_qsl
24
+ from urllib.parse import quote, unquote, parse_qsl
26
25
  import string
27
26
 
28
- from swift.common.utils import split_path, json, get_swift_info, \
29
- close_if_possible
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
- from swift.common.middleware.s3api.exception import NotS3Request, \
59
- BadSwiftRequest
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 HashingInput(object):
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 hash of the input as it's read.
138
+ wsgi.input wrapper to verify the SHA256 of the input as it's read.
122
139
  """
123
- def __init__(self, reader, content_length, hasher, expected_hex_hash):
124
- self._input = reader
125
- self._to_read = content_length
126
- self._hasher = hasher()
127
- self._expected = expected_hex_hash
128
-
129
- def read(self, size=None):
130
- chunk = self._input.read(size)
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
- self._to_read -= len(chunk)
133
- short_read = bool(chunk) if size is None else (len(chunk) < size)
134
- if self._to_read < 0 or (short_read and self._to_read) or (
135
- self._to_read == 0 and
136
- self._hasher.hexdigest() != self._expected):
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 swob.HTTPUnprocessableEntity(
140
- 'The X-Amz-Content-SHA56 you specified did not match '
141
- 'what we received.')
142
- return chunk
167
+ raise S3InputSHA256Mismatch(
168
+ self._expected_hash,
169
+ self._hasher.hexdigest())
143
170
 
144
- def close(self):
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 == valid_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 = self.params['X-Amz-SignedHeaders']
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 six.iteritems(self.headers))
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(self.environ.get('RAW_PATH_INFO', self.path))
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.upper())]
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
- if 'X-Amz-Credential' in self.params:
443
- # V4 with query parameters only
444
- hashed_payload = 'UNSIGNED-PAYLOAD'
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, slo_enabled=True, storage_domain='',
528
- location='us-east-1', force_request_log=False,
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.storage_domain = storage_domain
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.slo_enabled = slo_enabled
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).digest()).strip()
565
- if not six.PY2:
566
- valid_signature = valid_signature.decode('ascii')
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
- storage_domain = self.storage_domain
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
- if given_domain.endswith(storage_domain):
630
- return given_domain[:-len(storage_domain)]
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 (bucket, obj)
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 = 60 * 5
742
- if abs(int(self.timestamp) - int(S3Timestamp.now())) > delta:
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
- body = self.body_file.read(max_length)
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(body).digest()).strip().decode('ascii')
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
- if '?' in src_path:
893
- src_path, qs = src_path.split('?', 1)
894
- query = parse_qsl(qs, True)
895
- if not query:
896
- pass # ignore it
897
- elif len(query) > 1 or query[0][0] != 'versionId':
898
- raise InvalidArgument('X-Amz-Copy-Source',
899
- self.headers['X-Amz-Copy-Source'],
900
- 'Unsupported copy source parameter.')
901
- elif query[0][1] != 'null':
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, src_obj,
915
- headers=headers)
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.headers['X-Amz-Copy-Source'] = \
920
- '/' + self.headers['X-Amz-Copy-Source'].lstrip('/')
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.slo_enabled:
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
- return PartController
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', 'tagging', 'restore')
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
- if 'HTTP_X_AMZ_COPY_SOURCE' in env:
1125
- env['HTTP_X_COPY_FROM'] = env['HTTP_X_AMZ_COPY_SOURCE']
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.force_request_log:
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
- query_string = ''
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
- query_string = '&'.join(params)
1167
- env['QUERY_STRING'] = query_string
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: BadDigest,
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 swob.HTTPException as err:
1339
- sw_resp = err
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
- raise BadSwiftRequest(err_msg.decode('utf8'))
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
- # if we have already authenticated, yes we can use the account
1444
- # name like as AUTH_xxx for performance efficiency
1445
- sw_req = self.to_swift_req(app, self.container_name, None)
1446
- info = get_container_info(sw_req.environ, app)
1447
- if is_success(info['status']):
1448
- return info
1449
- elif info['status'] == 404:
1450
- raise NoSuchBucket(self.container_name)
1451
- else:
1452
- raise InternalError(
1453
- 'unexpected status code %d' % info['status'])
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
- # otherwise we do naive HEAD request with the authentication
1456
- resp = self.get_response(app, 'HEAD', self.container_name, '')
1457
- headers = resp.sw_headers.copy()
1458
- headers.update(resp.sysmeta_headers)
1459
- return headers_to_container_info(
1460
- headers, resp.status_int) # pylint: disable-msg=E1101
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
- resp = self.get_response(app, 'HEAD', obj=obj)
1469
- return query if resp.is_slo else None
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
- def __init__(self, env, app, slo_enabled=True, storage_domain='',
1480
- location='us-east-1', force_request_log=False,
1481
- dns_compliant_bucket_names=True, allow_multipart_uploads=True,
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