swift 2.32.0__py2.py3-none-any.whl → 2.34.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. swift/account/auditor.py +11 -0
  2. swift/account/reaper.py +11 -1
  3. swift/account/replicator.py +22 -0
  4. swift/account/server.py +13 -12
  5. swift-2.32.0.data/scripts/swift-account-audit → swift/cli/account_audit.py +6 -2
  6. swift-2.32.0.data/scripts/swift-config → swift/cli/config.py +1 -1
  7. swift-2.32.0.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +6 -2
  8. swift-2.32.0.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +12 -3
  9. swift-2.32.0.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +6 -2
  10. swift/cli/info.py +131 -3
  11. swift-2.32.0.data/scripts/swift-oldies → swift/cli/oldies.py +6 -3
  12. swift-2.32.0.data/scripts/swift-orphans → swift/cli/orphans.py +7 -2
  13. swift-2.32.0.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +9 -18
  14. swift-2.32.0.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
  15. swift/cli/relinker.py +1 -1
  16. swift/cli/reload.py +141 -0
  17. swift/cli/ringbuilder.py +24 -0
  18. swift/common/daemon.py +12 -2
  19. swift/common/db.py +14 -9
  20. swift/common/db_auditor.py +2 -2
  21. swift/common/db_replicator.py +6 -0
  22. swift/common/exceptions.py +12 -0
  23. swift/common/http_protocol.py +76 -3
  24. swift/common/manager.py +120 -5
  25. swift/common/memcached.py +24 -25
  26. swift/common/middleware/account_quotas.py +144 -43
  27. swift/common/middleware/backend_ratelimit.py +166 -24
  28. swift/common/middleware/catch_errors.py +1 -3
  29. swift/common/middleware/cname_lookup.py +3 -5
  30. swift/common/middleware/container_sync.py +6 -10
  31. swift/common/middleware/crypto/crypto_utils.py +4 -5
  32. swift/common/middleware/crypto/decrypter.py +4 -5
  33. swift/common/middleware/crypto/kms_keymaster.py +2 -1
  34. swift/common/middleware/proxy_logging.py +57 -43
  35. swift/common/middleware/ratelimit.py +6 -7
  36. swift/common/middleware/recon.py +6 -7
  37. swift/common/middleware/s3api/acl_handlers.py +10 -1
  38. swift/common/middleware/s3api/controllers/__init__.py +3 -0
  39. swift/common/middleware/s3api/controllers/acl.py +3 -2
  40. swift/common/middleware/s3api/controllers/logging.py +2 -2
  41. swift/common/middleware/s3api/controllers/multi_upload.py +31 -15
  42. swift/common/middleware/s3api/controllers/obj.py +20 -1
  43. swift/common/middleware/s3api/controllers/object_lock.py +44 -0
  44. swift/common/middleware/s3api/s3api.py +6 -0
  45. swift/common/middleware/s3api/s3request.py +190 -74
  46. swift/common/middleware/s3api/s3response.py +48 -8
  47. swift/common/middleware/s3api/s3token.py +2 -2
  48. swift/common/middleware/s3api/utils.py +2 -1
  49. swift/common/middleware/slo.py +508 -310
  50. swift/common/middleware/staticweb.py +45 -14
  51. swift/common/middleware/tempauth.py +6 -4
  52. swift/common/middleware/tempurl.py +134 -93
  53. swift/common/middleware/x_profile/exceptions.py +1 -4
  54. swift/common/middleware/x_profile/html_viewer.py +9 -10
  55. swift/common/middleware/x_profile/profile_model.py +1 -2
  56. swift/common/middleware/xprofile.py +1 -2
  57. swift/common/request_helpers.py +101 -8
  58. swift/common/statsd_client.py +207 -0
  59. swift/common/storage_policy.py +1 -1
  60. swift/common/swob.py +5 -2
  61. swift/common/utils/__init__.py +331 -1774
  62. swift/common/utils/base.py +138 -0
  63. swift/common/utils/config.py +443 -0
  64. swift/common/utils/logs.py +999 -0
  65. swift/common/utils/timestamp.py +23 -2
  66. swift/common/wsgi.py +19 -3
  67. swift/container/auditor.py +11 -0
  68. swift/container/backend.py +136 -31
  69. swift/container/reconciler.py +11 -2
  70. swift/container/replicator.py +64 -7
  71. swift/container/server.py +276 -146
  72. swift/container/sharder.py +86 -42
  73. swift/container/sync.py +11 -1
  74. swift/container/updater.py +12 -2
  75. swift/obj/auditor.py +20 -3
  76. swift/obj/diskfile.py +63 -25
  77. swift/obj/expirer.py +154 -47
  78. swift/obj/mem_diskfile.py +2 -1
  79. swift/obj/mem_server.py +1 -0
  80. swift/obj/reconstructor.py +28 -4
  81. swift/obj/replicator.py +63 -24
  82. swift/obj/server.py +76 -59
  83. swift/obj/updater.py +12 -2
  84. swift/obj/watchers/dark_data.py +72 -34
  85. swift/proxy/controllers/account.py +3 -2
  86. swift/proxy/controllers/base.py +254 -148
  87. swift/proxy/controllers/container.py +274 -289
  88. swift/proxy/controllers/obj.py +120 -166
  89. swift/proxy/server.py +17 -13
  90. {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/AUTHORS +14 -4
  91. {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/METADATA +9 -7
  92. {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/RECORD +97 -120
  93. {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/entry_points.txt +39 -0
  94. swift-2.34.0.dist-info/pbr.json +1 -0
  95. swift-2.32.0.data/scripts/swift-account-auditor +0 -23
  96. swift-2.32.0.data/scripts/swift-account-info +0 -52
  97. swift-2.32.0.data/scripts/swift-account-reaper +0 -23
  98. swift-2.32.0.data/scripts/swift-account-replicator +0 -34
  99. swift-2.32.0.data/scripts/swift-account-server +0 -23
  100. swift-2.32.0.data/scripts/swift-container-auditor +0 -23
  101. swift-2.32.0.data/scripts/swift-container-info +0 -56
  102. swift-2.32.0.data/scripts/swift-container-reconciler +0 -21
  103. swift-2.32.0.data/scripts/swift-container-replicator +0 -34
  104. swift-2.32.0.data/scripts/swift-container-server +0 -23
  105. swift-2.32.0.data/scripts/swift-container-sharder +0 -37
  106. swift-2.32.0.data/scripts/swift-container-sync +0 -23
  107. swift-2.32.0.data/scripts/swift-container-updater +0 -23
  108. swift-2.32.0.data/scripts/swift-dispersion-report +0 -24
  109. swift-2.32.0.data/scripts/swift-form-signature +0 -20
  110. swift-2.32.0.data/scripts/swift-init +0 -119
  111. swift-2.32.0.data/scripts/swift-object-auditor +0 -29
  112. swift-2.32.0.data/scripts/swift-object-expirer +0 -33
  113. swift-2.32.0.data/scripts/swift-object-info +0 -60
  114. swift-2.32.0.data/scripts/swift-object-reconstructor +0 -33
  115. swift-2.32.0.data/scripts/swift-object-relinker +0 -23
  116. swift-2.32.0.data/scripts/swift-object-replicator +0 -37
  117. swift-2.32.0.data/scripts/swift-object-server +0 -27
  118. swift-2.32.0.data/scripts/swift-object-updater +0 -23
  119. swift-2.32.0.data/scripts/swift-proxy-server +0 -23
  120. swift-2.32.0.data/scripts/swift-recon +0 -24
  121. swift-2.32.0.data/scripts/swift-ring-builder +0 -37
  122. swift-2.32.0.data/scripts/swift-ring-builder-analyzer +0 -22
  123. swift-2.32.0.data/scripts/swift-ring-composer +0 -22
  124. swift-2.32.0.dist-info/pbr.json +0 -1
  125. {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/LICENSE +0 -0
  126. {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/WHEEL +0 -0
  127. {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/top_level.txt +0 -0
@@ -26,7 +26,7 @@ from six.moves.urllib.parse import quote, unquote, parse_qsl
26
26
  import string
27
27
 
28
28
  from swift.common.utils import split_path, json, close_if_possible, md5, \
29
- streq_const_time
29
+ streq_const_time, get_policy_index
30
30
  from swift.common.registry import get_swift_info
31
31
  from swift.common import swob
32
32
  from swift.common.http import HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, \
@@ -47,7 +47,7 @@ from swift.common.middleware.s3api.controllers import ServiceController, \
47
47
  LocationController, LoggingStatusController, PartController, \
48
48
  UploadController, UploadsController, VersioningController, \
49
49
  UnsupportedController, S3AclController, BucketController, \
50
- TaggingController
50
+ TaggingController, ObjectLockController
51
51
  from swift.common.middleware.s3api.s3response import AccessDenied, \
52
52
  InvalidArgument, InvalidDigest, BucketAlreadyOwnedByYou, \
53
53
  RequestTimeTooSkewed, S3Response, SignatureDoesNotMatch, \
@@ -56,7 +56,8 @@ from swift.common.middleware.s3api.s3response import AccessDenied, \
56
56
  MissingContentLength, InvalidStorageClass, S3NotImplemented, InvalidURI, \
57
57
  MalformedXML, InvalidRequest, RequestTimeout, InvalidBucketName, \
58
58
  BadDigest, AuthorizationHeaderMalformed, SlowDown, \
59
- AuthorizationQueryParametersError, ServiceUnavailable, BrokenMPU
59
+ AuthorizationQueryParametersError, ServiceUnavailable, BrokenMPU, \
60
+ InvalidPartNumber, InvalidPartArgument, XAmzContentSHA256Mismatch
60
61
  from swift.common.middleware.s3api.exception import NotS3Request
61
62
  from swift.common.middleware.s3api.utils import utf8encode, \
62
63
  S3Timestamp, mktime, MULTIUPLOAD_SUFFIX
@@ -74,7 +75,8 @@ ALLOWED_SUB_RESOURCES = sorted([
74
75
  'versionId', 'versioning', 'versions', 'website',
75
76
  'response-cache-control', 'response-content-disposition',
76
77
  'response-content-encoding', 'response-content-language',
77
- 'response-content-type', 'response-expires', 'cors', 'tagging', 'restore'
78
+ 'response-content-type', 'response-expires', 'cors', 'tagging', 'restore',
79
+ 'object-lock'
78
80
  ])
79
81
 
80
82
 
@@ -103,6 +105,7 @@ def _header_acl_property(resource):
103
105
  """
104
106
  Set and retrieve the acl in self.headers
105
107
  """
108
+
106
109
  def getter(self):
107
110
  return getattr(self, '_%s' % resource)
108
111
 
@@ -117,15 +120,37 @@ def _header_acl_property(resource):
117
120
  doc='Get and set the %s acl property' % resource)
118
121
 
119
122
 
123
+ class S3InputSHA256Mismatch(BaseException):
124
+ """
125
+ Client provided a X-Amz-Content-SHA256, but it doesn't match the data.
126
+
127
+ Inherit from BaseException (rather than Exception) so it cuts from the
128
+ proxy-server app (which will presumably be the one reading the input)
129
+ through all the layers of the pipeline back to us. It should never escape
130
+ the s3api middleware.
131
+ """
132
+ def __init__(self, expected, computed):
133
+ self.expected = expected
134
+ self.computed = computed
135
+
136
+
120
137
  class HashingInput(object):
121
138
  """
122
139
  wsgi.input wrapper to verify the hash of the input as it's read.
123
140
  """
141
+
124
142
  def __init__(self, reader, content_length, hasher, expected_hex_hash):
125
143
  self._input = reader
126
144
  self._to_read = content_length
127
145
  self._hasher = hasher()
128
146
  self._expected = expected_hex_hash
147
+ if content_length == 0 and \
148
+ self._hasher.hexdigest() != self._expected.lower():
149
+ self.close()
150
+ raise XAmzContentSHA256Mismatch(
151
+ client_computed_content_s_h_a256=self._expected,
152
+ s3_computed_content_s_h_a256=self._hasher.hexdigest(),
153
+ )
129
154
 
130
155
  def read(self, size=None):
131
156
  chunk = self._input.read(size)
@@ -134,12 +159,12 @@ class HashingInput(object):
134
159
  short_read = bool(chunk) if size is None else (len(chunk) < size)
135
160
  if self._to_read < 0 or (short_read and self._to_read) or (
136
161
  self._to_read == 0 and
137
- self._hasher.hexdigest() != self._expected):
162
+ self._hasher.hexdigest() != self._expected.lower()):
138
163
  self.close()
139
164
  # Since we don't return the last chunk, the PUT never completes
140
- raise swob.HTTPUnprocessableEntity(
141
- 'The X-Amz-Content-SHA56 you specified did not match '
142
- 'what we received.')
165
+ raise S3InputSHA256Mismatch(
166
+ self._expected,
167
+ self._hasher.hexdigest())
143
168
  return chunk
144
169
 
145
170
  def close(self):
@@ -234,6 +259,28 @@ class SigV4Mixin(object):
234
259
  if int(self.timestamp) + expires < S3Timestamp.now():
235
260
  raise AccessDenied('Request has expired', reason='expired')
236
261
 
262
+ def _validate_sha256(self):
263
+ aws_sha256 = self.headers.get('x-amz-content-sha256')
264
+ looks_like_sha256 = (
265
+ aws_sha256 and len(aws_sha256) == 64 and
266
+ all(c in '0123456789abcdef' for c in aws_sha256.lower()))
267
+ if not aws_sha256:
268
+ if 'X-Amz-Credential' in self.params:
269
+ pass # pre-signed URL; not required
270
+ else:
271
+ msg = 'Missing required header for this request: ' \
272
+ 'x-amz-content-sha256'
273
+ raise InvalidRequest(msg)
274
+ elif aws_sha256 == 'UNSIGNED-PAYLOAD':
275
+ pass
276
+ elif not looks_like_sha256 and 'X-Amz-Credential' not in self.params:
277
+ raise InvalidArgument(
278
+ 'x-amz-content-sha256',
279
+ aws_sha256,
280
+ 'x-amz-content-sha256 must be UNSIGNED-PAYLOAD, or '
281
+ 'a valid sha256 value.')
282
+ return aws_sha256
283
+
237
284
  def _parse_credential(self, credential_string):
238
285
  parts = credential_string.split("/")
239
286
  # credential must be in following format:
@@ -444,30 +491,9 @@ class SigV4Mixin(object):
444
491
  cr.append(b';'.join(swob.wsgi_to_bytes(k) for k, v in headers_to_sign))
445
492
 
446
493
  # 6. Add payload string at the tail
447
- if 'X-Amz-Credential' in self.params:
448
- # V4 with query parameters only
449
- hashed_payload = 'UNSIGNED-PAYLOAD'
450
- elif 'X-Amz-Content-SHA256' not in self.headers:
451
- msg = 'Missing required header for this request: ' \
452
- 'x-amz-content-sha256'
453
- raise InvalidRequest(msg)
454
- else:
455
- hashed_payload = self.headers['X-Amz-Content-SHA256']
456
- if hashed_payload != 'UNSIGNED-PAYLOAD':
457
- if self.content_length == 0:
458
- if hashed_payload.lower() != sha256().hexdigest():
459
- raise BadDigest(
460
- 'The X-Amz-Content-SHA56 you specified did not '
461
- 'match what we received.')
462
- elif self.content_length:
463
- self.environ['wsgi.input'] = HashingInput(
464
- self.environ['wsgi.input'],
465
- self.content_length,
466
- sha256,
467
- hashed_payload.lower())
468
- # else, length not provided -- Swift will kick out a
469
- # 411 Length Required which will get translated back
470
- # to a S3-style response in S3Request._swift_error_codes
494
+ hashed_payload = self.headers.get('X-Amz-Content-SHA256',
495
+ 'UNSIGNED-PAYLOAD')
496
+
471
497
  cr.append(swob.wsgi_to_bytes(hashed_payload))
472
498
  return b'\n'.join(cr)
473
499
 
@@ -549,11 +575,63 @@ class S3Request(swob.Request):
549
575
  }
550
576
  self.account = None
551
577
  self.user_id = None
578
+ self.policy_index = None
552
579
 
553
580
  # Avoids that swift.swob.Response replaces Location header value
554
581
  # by full URL when absolute path given. See swift.swob for more detail.
555
582
  self.environ['swift.leave_relative_location'] = True
556
583
 
584
+ def validate_part_number(self, parts_count=None, check_max=True):
585
+ """
586
+ Get the partNumber param, if it exists, and check it is valid.
587
+
588
+ To be valid, a partNumber must satisfy two criteria. First, it must be
589
+ an integer between 1 and the maximum allowed parts, inclusive. The
590
+ maximum allowed parts is the maximum of the configured
591
+ ``max_upload_part_num`` and, if given, ``parts_count``. Second, the
592
+ partNumber must be less than or equal to the ``parts_count``, if it is
593
+ given.
594
+
595
+ :param parts_count: if given, this is the number of parts in an
596
+ existing object.
597
+ :raises InvalidPartArgument: if the partNumber param is invalid i.e.
598
+ less than 1 or greater than the maximum allowed parts.
599
+ :raises InvalidPartNumber: if the partNumber param is valid but greater
600
+ than ``num_parts``.
601
+ :return: an integer part number if the partNumber param exists,
602
+ otherwise ``None``.
603
+ """
604
+ part_number = self.params.get('partNumber')
605
+ if part_number is None:
606
+ return None
607
+
608
+ if self.range:
609
+ raise InvalidRequest('Cannot specify both Range header and '
610
+ 'partNumber query parameter')
611
+
612
+ try:
613
+ parts_count = int(parts_count)
614
+ except (TypeError, ValueError):
615
+ # an invalid/empty param is treated like parts_count=max_parts
616
+ parts_count = self.conf.max_upload_part_num
617
+ # max_parts may be raised to the number of existing parts
618
+ max_parts = max(self.conf.max_upload_part_num, parts_count)
619
+
620
+ try:
621
+ part_number = int(part_number)
622
+ if part_number < 1:
623
+ raise ValueError
624
+ except ValueError:
625
+ raise InvalidPartArgument(max_parts, part_number) # 400
626
+
627
+ if check_max:
628
+ if part_number > max_parts:
629
+ raise InvalidPartArgument(max_parts, part_number) # 400
630
+ if part_number > parts_count:
631
+ raise InvalidPartNumber() # 416
632
+
633
+ return part_number
634
+
557
635
  def check_signature(self, secret):
558
636
  secret = utf8encode(secret)
559
637
  user_signature = self.signature
@@ -743,6 +821,9 @@ class S3Request(swob.Request):
743
821
  if delta > self.conf.allowable_clock_skew:
744
822
  raise RequestTimeTooSkewed()
745
823
 
824
+ def _validate_sha256(self):
825
+ return self.headers.get('x-amz-content-sha256')
826
+
746
827
  def _validate_headers(self):
747
828
  if 'CONTENT_LENGTH' in self.environ:
748
829
  try:
@@ -753,21 +834,6 @@ class S3Request(swob.Request):
753
834
  raise InvalidArgument('Content-Length',
754
835
  self.environ['CONTENT_LENGTH'])
755
836
 
756
- value = _header_strip(self.headers.get('Content-MD5'))
757
- if value is not None:
758
- if not re.match('^[A-Za-z0-9+/]+={0,2}$', value):
759
- # Non-base64-alphabet characters in value.
760
- raise InvalidDigest(content_md5=value)
761
- try:
762
- self.headers['ETag'] = binascii.b2a_hex(
763
- binascii.a2b_base64(value))
764
- except binascii.Error:
765
- # incorrect padding, most likely
766
- raise InvalidDigest(content_md5=value)
767
-
768
- if len(self.headers['ETag']) != 32:
769
- raise InvalidDigest(content_md5=value)
770
-
771
837
  if self.method == 'PUT' and any(h in self.headers for h in (
772
838
  'If-Match', 'If-None-Match',
773
839
  'If-Modified-Since', 'If-Unmodified-Since')):
@@ -813,6 +879,38 @@ class S3Request(swob.Request):
813
879
  if 'x-amz-website-redirect-location' in self.headers:
814
880
  raise S3NotImplemented('Website redirection is not supported.')
815
881
 
882
+ aws_sha256 = self._validate_sha256()
883
+ if (aws_sha256
884
+ and aws_sha256 != 'UNSIGNED-PAYLOAD'
885
+ and self.content_length is not None):
886
+ # Even if client-provided SHA doesn't look like a SHA, wrap the
887
+ # input anyway so we'll send the SHA of what the client sent in
888
+ # the eventual error
889
+ self.environ['wsgi.input'] = HashingInput(
890
+ self.environ['wsgi.input'],
891
+ self.content_length,
892
+ sha256,
893
+ aws_sha256)
894
+ # If no content-length, either client's trying to do a HTTP chunked
895
+ # transfer, or a HTTP/1.0-style transfer (in which case swift will
896
+ # reject with length-required and we'll translate back to
897
+ # MissingContentLength)
898
+
899
+ value = _header_strip(self.headers.get('Content-MD5'))
900
+ if value is not None:
901
+ if not re.match('^[A-Za-z0-9+/]+={0,2}$', value):
902
+ # Non-base64-alphabet characters in value.
903
+ raise InvalidDigest(content_md5=value)
904
+ try:
905
+ self.headers['ETag'] = binascii.b2a_hex(
906
+ binascii.a2b_base64(value))
907
+ except binascii.Error:
908
+ # incorrect padding, most likely
909
+ raise InvalidDigest(content_md5=value)
910
+
911
+ if len(self.headers['ETag']) != 32:
912
+ raise InvalidDigest(content_md5=value)
913
+
816
914
  # https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
817
915
  # describes some of what would be required to support this
818
916
  if any(['aws-chunked' in self.headers.get('content-encoding', ''),
@@ -854,13 +952,11 @@ class S3Request(swob.Request):
854
952
  # Limit the read similar to how SLO handles manifests
855
953
  try:
856
954
  body = self.body_file.read(max_length)
857
- except swob.HTTPException as err:
858
- if err.status_int == HTTP_UNPROCESSABLE_ENTITY:
859
- # Special case for HashingInput check
860
- raise BadDigest(
861
- 'The X-Amz-Content-SHA56 you specified did not '
862
- 'match what we received.')
863
- raise
955
+ except S3InputSHA256Mismatch as err:
956
+ raise XAmzContentSHA256Mismatch(
957
+ client_computed_content_s_h_a256=err.expected,
958
+ s3_computed_content_s_h_a256=err.computed,
959
+ )
864
960
  else:
865
961
  # No (or zero) Content-Length provided, and not chunked transfer;
866
962
  # no body. Assume zero-length, and enforce a required body below.
@@ -919,8 +1015,6 @@ class S3Request(swob.Request):
919
1015
  src_resp = self.get_response(app, 'HEAD', src_bucket,
920
1016
  swob.str_to_wsgi(src_obj),
921
1017
  headers=headers, query=query)
922
- # we can't let this HEAD req spoil our COPY
923
- self.headers.pop('x-backend-storage-policy-index')
924
1018
  if src_resp.status_int == 304: # pylint: disable-msg=E1101
925
1019
  raise PreconditionFailed()
926
1020
 
@@ -1042,7 +1136,10 @@ class S3Request(swob.Request):
1042
1136
  if 'logging' in self.params:
1043
1137
  return LoggingStatusController
1044
1138
  if 'partNumber' in self.params:
1045
- return PartController
1139
+ if self.method == 'PUT':
1140
+ return PartController
1141
+ else:
1142
+ return ObjectController
1046
1143
  if 'uploadId' in self.params:
1047
1144
  return UploadController
1048
1145
  if 'uploads' in self.params:
@@ -1051,6 +1148,8 @@ class S3Request(swob.Request):
1051
1148
  return VersioningController
1052
1149
  if 'tagging' in self.params:
1053
1150
  return TaggingController
1151
+ if 'object-lock' in self.params:
1152
+ return ObjectLockController
1054
1153
 
1055
1154
  unsupported = ('notification', 'policy', 'requestPayment', 'torrent',
1056
1155
  'website', 'cors', 'restore')
@@ -1083,7 +1182,7 @@ class S3Request(swob.Request):
1083
1182
  Create a Swift request based on this request's environment.
1084
1183
  """
1085
1184
  if self.account is None:
1086
- account = self.access_key
1185
+ account = swob.str_to_wsgi(self.access_key)
1087
1186
  else:
1088
1187
  account = self.account
1089
1188
 
@@ -1303,6 +1402,16 @@ class S3Request(swob.Request):
1303
1402
  return NoSuchKey(obj)
1304
1403
  return NoSuchBucket(container)
1305
1404
 
1405
+ # Since BadDigest ought to plumb in some client-provided values,
1406
+ # defer evaluation until we know they're provided
1407
+ def bad_digest_handler():
1408
+ etag = binascii.hexlify(base64.b64decode(
1409
+ env['HTTP_CONTENT_MD5']))
1410
+ return BadDigest(
1411
+ expected_digest=etag, # yes, really hex
1412
+ # TODO: plumb in calculated_digest, as b64
1413
+ )
1414
+
1306
1415
  code_map = {
1307
1416
  'HEAD': {
1308
1417
  HTTP_NOT_FOUND: not_found_handler,
@@ -1311,11 +1420,10 @@ class S3Request(swob.Request):
1311
1420
  'GET': {
1312
1421
  HTTP_NOT_FOUND: not_found_handler,
1313
1422
  HTTP_PRECONDITION_FAILED: PreconditionFailed,
1314
- HTTP_REQUESTED_RANGE_NOT_SATISFIABLE: InvalidRange,
1315
1423
  },
1316
1424
  'PUT': {
1317
1425
  HTTP_NOT_FOUND: (NoSuchBucket, container),
1318
- HTTP_UNPROCESSABLE_ENTITY: BadDigest,
1426
+ HTTP_UNPROCESSABLE_ENTITY: bad_digest_handler,
1319
1427
  HTTP_REQUEST_ENTITY_TOO_LARGE: EntityTooLarge,
1320
1428
  HTTP_LENGTH_REQUIRED: MissingContentLength,
1321
1429
  HTTP_REQUEST_TIMEOUT: RequestTimeout,
@@ -1352,24 +1460,25 @@ class S3Request(swob.Request):
1352
1460
 
1353
1461
  try:
1354
1462
  sw_resp = sw_req.get_response(app)
1355
- except swob.HTTPException as err:
1356
- # Maybe a 422 from HashingInput? Put something in
1357
- # s3api.backend_path - hopefully by now any modifications to the
1358
- # path (e.g. tenant to account translation) will have been made by
1359
- # auth middleware
1463
+ except S3InputSHA256Mismatch as err:
1464
+ # hopefully by now any modifications to the path (e.g. tenant to
1465
+ # account translation) will have been made by auth middleware
1360
1466
  self.environ['s3api.backend_path'] = sw_req.environ['PATH_INFO']
1361
- sw_resp = err
1467
+ raise XAmzContentSHA256Mismatch(
1468
+ client_computed_content_s_h_a256=err.expected,
1469
+ s3_computed_content_s_h_a256=err.computed,
1470
+ )
1362
1471
  else:
1363
1472
  # reuse account
1364
1473
  _, self.account, _ = split_path(sw_resp.environ['PATH_INFO'],
1365
1474
  2, 3, True)
1366
1475
  # Update s3.backend_path from the response environ
1367
1476
  self.environ['s3api.backend_path'] = sw_resp.environ['PATH_INFO']
1368
- # Propogate backend headers back into our req headers for logging
1369
- for k, v in sw_req.headers.items():
1370
- if k.lower().startswith('x-backend-'):
1371
- self.headers.setdefault(k, v)
1372
1477
 
1478
+ # keep a record of the backend policy index so that the s3api can add
1479
+ # it to the headers of whatever response it returns, which may not
1480
+ # necessarily be this resp.
1481
+ self.policy_index = get_policy_index(sw_req.headers, sw_resp.headers)
1373
1482
  resp = S3Response.from_swift_resp(sw_resp)
1374
1483
  status = resp.status_int # pylint: disable-msg=E1101
1375
1484
 
@@ -1410,7 +1519,7 @@ class S3Request(swob.Request):
1410
1519
  raise InvalidArgument('X-Delete-At',
1411
1520
  self.headers['X-Delete-At'],
1412
1521
  err_str)
1413
- if 'X-Delete-After' in err_msg.decode('utf8'):
1522
+ if 'X-Delete-After' in err_str:
1414
1523
  raise InvalidArgument('X-Delete-After',
1415
1524
  self.headers['X-Delete-After'],
1416
1525
  err_str)
@@ -1421,6 +1530,10 @@ class S3Request(swob.Request):
1421
1530
  **self.signature_does_not_match_kwargs())
1422
1531
  if status == HTTP_FORBIDDEN:
1423
1532
  raise AccessDenied(reason='forbidden')
1533
+ if status == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
1534
+ self.validate_part_number(
1535
+ parts_count=resp.headers.get('x-amz-mp-parts-count'))
1536
+ raise InvalidRange()
1424
1537
  if status == HTTP_SERVICE_UNAVAILABLE:
1425
1538
  raise ServiceUnavailable()
1426
1539
  if status in (HTTP_RATE_LIMITED, HTTP_TOO_MANY_REQUESTS):
@@ -1428,8 +1541,10 @@ class S3Request(swob.Request):
1428
1541
  raise SlowDown(status='429 Slow Down')
1429
1542
  raise SlowDown()
1430
1543
  if resp.status_int == HTTP_CONFLICT:
1431
- # TODO: validate that this actually came up out of SLO
1432
- raise BrokenMPU()
1544
+ if self.method == 'GET':
1545
+ raise BrokenMPU()
1546
+ else:
1547
+ raise ServiceUnavailable()
1433
1548
 
1434
1549
  raise InternalError('unexpected status code %d' % status)
1435
1550
 
@@ -1497,7 +1612,7 @@ class S3Request(swob.Request):
1497
1612
 
1498
1613
  _, self.account, _ = split_path(sw_resp.environ['PATH_INFO'],
1499
1614
  2, 3, True)
1500
- sw_req = self.to_swift_req(app, self.container_name, None)
1615
+ sw_req = self.to_swift_req('TEST', self.container_name, None)
1501
1616
  info = get_container_info(sw_req.environ, app, swift_source='S3')
1502
1617
  if is_success(info['status']):
1503
1618
  return info
@@ -1536,6 +1651,7 @@ class S3AclRequest(S3Request):
1536
1651
  """
1537
1652
  S3Acl request object.
1538
1653
  """
1654
+
1539
1655
  def __init__(self, env, app=None, conf=None):
1540
1656
  super(S3AclRequest, self).__init__(env, app, conf)
1541
1657
  self.authenticate(app)
@@ -64,7 +64,7 @@ def translate_swift_to_s3(key, val):
64
64
 
65
65
  if _key.startswith('x-object-meta-'):
66
66
  return translate_meta_key(_key), val
67
- elif _key in ('content-length', 'content-type',
67
+ elif _key in ('accept-ranges', 'content-length', 'content-type',
68
68
  'content-range', 'content-encoding',
69
69
  'content-disposition', 'content-language',
70
70
  'etag', 'last-modified', 'x-robots-tag',
@@ -72,6 +72,8 @@ def translate_swift_to_s3(key, val):
72
72
  return key, val
73
73
  elif _key == 'x-object-version-id':
74
74
  return 'x-amz-version-id', val
75
+ elif _key == 'x-parts-count':
76
+ return 'x-amz-mp-parts-count', val
75
77
  elif _key == 'x-copied-from-version-id':
76
78
  return 'x-amz-copy-source-version-id', val
77
79
  elif _key == 'x-backend-content-type' and \
@@ -111,6 +113,7 @@ class S3Response(S3ResponseBase, swob.Response):
111
113
  headers instead of Swift's HeaderKeyDict. This also translates Swift
112
114
  specific headers to S3 headers.
113
115
  """
116
+
114
117
  def __init__(self, *args, **kwargs):
115
118
  swob.Response.__init__(self, *args, **kwargs)
116
119
 
@@ -239,7 +242,7 @@ class ErrorResponse(S3ResponseBase, swob.HTTPException):
239
242
  self.info = kwargs.copy()
240
243
  for reserved_key in ('headers', 'body'):
241
244
  if self.info.get(reserved_key):
242
- del(self.info[reserved_key])
245
+ del (self.info[reserved_key])
243
246
 
244
247
  swob.HTTPException.__init__(
245
248
  self, status=kwargs.pop('status', self._status),
@@ -249,12 +252,17 @@ class ErrorResponse(S3ResponseBase, swob.HTTPException):
249
252
  self.headers = HeaderKeyDict(self.headers)
250
253
 
251
254
  @property
252
- def metric_name(self):
253
- parts = [str(self.status_int), self._code]
255
+ def summary(self):
256
+ """Provide a summary of the error code and reason."""
254
257
  if self.reason:
255
- parts.append(self.reason)
256
- metric = '.'.join(parts)
257
- return metric.replace(' ', '_')
258
+ summary = '.'.join([self._code, self.reason])
259
+ else:
260
+ summary = self._code
261
+ return summary.replace(' ', '_')
262
+
263
+ @property
264
+ def metric_name(self):
265
+ return '.'.join([str(self.status_int), self.summary])
258
266
 
259
267
  def _body_iter(self):
260
268
  error_elem = Element('Error')
@@ -319,6 +327,12 @@ class BadDigest(ErrorResponse):
319
327
  _msg = 'The Content-MD5 you specified did not match what we received.'
320
328
 
321
329
 
330
+ class XAmzContentSHA256Mismatch(ErrorResponse):
331
+ _status = '400 Bad Request'
332
+ _msg = "The provided 'x-amz-content-sha256' header does not match what " \
333
+ "was computed."
334
+
335
+
322
336
  class BucketAlreadyExists(ErrorResponse):
323
337
  _status = '409 Conflict'
324
338
  _msg = 'The requested bucket name is not available. The bucket ' \
@@ -435,7 +449,7 @@ class InvalidBucketState(ErrorResponse):
435
449
 
436
450
  class InvalidDigest(ErrorResponse):
437
451
  _status = '400 Bad Request'
438
- _msg = 'The Content-MD5 you specified was an invalid.'
452
+ _msg = 'The Content-MD5 you specified was invalid.'
439
453
 
440
454
 
441
455
  class InvalidLocationConstraint(ErrorResponse):
@@ -448,6 +462,17 @@ class InvalidObjectState(ErrorResponse):
448
462
  _msg = 'The operation is not valid for the current state of the object.'
449
463
 
450
464
 
465
+ class InvalidPartArgument(InvalidArgument):
466
+ _code = 'InvalidArgument'
467
+
468
+ def __init__(self, max_parts, value):
469
+ err_msg = ('Part number must be an integer between '
470
+ '1 and %s, inclusive' % max_parts)
471
+ super(InvalidArgument, self).__init__(err_msg,
472
+ argument_name='partNumber',
473
+ argument_value=value)
474
+
475
+
451
476
  class InvalidPart(ErrorResponse):
452
477
  _status = '400 Bad Request'
453
478
  _msg = 'One or more of the specified parts could not be found. The part ' \
@@ -477,6 +502,11 @@ class InvalidRange(ErrorResponse):
477
502
  _msg = 'The requested range cannot be satisfied.'
478
503
 
479
504
 
505
+ class InvalidPartNumber(ErrorResponse):
506
+ _status = '416 Requested Range Not Satisfiable'
507
+ _msg = 'The requested partnumber is not satisfiable'
508
+
509
+
480
510
  class InvalidRequest(ErrorResponse):
481
511
  _status = '400 Bad Request'
482
512
  _msg = 'Invalid Request.'
@@ -613,6 +643,16 @@ class NoSuchKey(ErrorResponse):
613
643
  ErrorResponse.__init__(self, msg, key=key, *args, **kwargs)
614
644
 
615
645
 
646
+ class ObjectLockConfigurationNotFoundError(ErrorResponse):
647
+ _status = '404 Not found'
648
+ _msg = 'Object Lock configuration does not exist for this bucket'
649
+
650
+ def __init__(self, bucket, msg=None, *args, **kwargs):
651
+ if not bucket:
652
+ raise InternalError()
653
+ ErrorResponse.__init__(self, msg, bucket_name=bucket, *args, **kwargs)
654
+
655
+
616
656
  class NoSuchLifecycleConfiguration(ErrorResponse):
617
657
  _status = '404 Not Found'
618
658
  _msg = 'The lifecycle configuration does not exist. .'
@@ -65,7 +65,7 @@ import six
65
65
  from six.moves import urllib
66
66
 
67
67
  from swift.common.swob import Request, HTTPBadRequest, HTTPUnauthorized, \
68
- HTTPException
68
+ HTTPException, str_to_wsgi
69
69
  from swift.common.utils import config_true_value, split_path, get_logger, \
70
70
  cache_from_env, append_underscore
71
71
  from swift.common.wsgi import ConfigFileError
@@ -404,7 +404,7 @@ class S3Token(object):
404
404
  self._logger.debug('Connecting with tenant: %s', tenant_to_connect)
405
405
  new_tenant_name = '%s%s' % (self._reseller_prefix, tenant_to_connect)
406
406
  environ['PATH_INFO'] = environ['PATH_INFO'].replace(
407
- account, new_tenant_name, 1)
407
+ str_to_wsgi(account), str_to_wsgi(new_tenant_name), 1)
408
408
  return self._app(environ, start_response)
409
409
 
410
410
 
@@ -113,7 +113,7 @@ class S3Timestamp(utils.Timestamp):
113
113
 
114
114
  @property
115
115
  def s3xmlformat(self):
116
- dt = datetime.datetime.utcfromtimestamp(self.ceil())
116
+ dt = datetime.datetime.fromtimestamp(self.ceil(), utils.UTC)
117
117
  return dt.strftime(self.S3_XML_FORMAT)
118
118
 
119
119
  @classmethod
@@ -172,6 +172,7 @@ class Config(dict):
172
172
  'allow_no_owner': False,
173
173
  'allowable_clock_skew': 900,
174
174
  'ratelimit_as_client_error': False,
175
+ 'max_upload_part_num': 1000,
175
176
  }
176
177
 
177
178
  def __init__(self, base=None):