Merge "Don't try to read a body if the client didn't send one"

This commit is contained in:
Jenkins 2017-10-10 20:19:20 +00:00 committed by Gerrit Code Review
commit 5dc988d1f6
10 changed files with 178 additions and 23 deletions

View File

@ -61,8 +61,7 @@ def get_acl(headers, body, bucket_owner, object_owner=None):
if acl is None: if acl is None:
# Get acl from request body if possible. # Get acl from request body if possible.
if not body: if not body:
msg = 'Your request was missing a required header' raise MissingSecurityHeader(missing_header_name='x-amz-acl')
raise MissingSecurityHeader(msg, missing_header_name='x-amz-acl')
try: try:
elem = fromstring(body, ACL.root_tag) elem = fromstring(body, ACL.root_tag)
acl = ACL.from_elem(elem) acl = ACL.from_elem(elem)

View File

@ -20,7 +20,7 @@ from swift.common.utils import public
from swift3.exception import ACLError from swift3.exception import ACLError
from swift3.controllers.base import Controller from swift3.controllers.base import Controller
from swift3.response import HTTPOk, S3NotImplemented, MalformedACLError, \ from swift3.response import HTTPOk, S3NotImplemented, MalformedACLError, \
UnexpectedContent UnexpectedContent, MissingSecurityHeader
from swift3.etree import Element, SubElement, tostring from swift3.etree import Element, SubElement, tostring
from swift3.acl_utils import swift_acl_translate, XMLNS_XSI from swift3.acl_utils import swift_acl_translate, XMLNS_XSI
@ -103,18 +103,24 @@ class AclController(Controller):
else: else:
# Handle Bucket ACL # Handle Bucket ACL
xml = req.xml(MAX_ACL_BODY_SIZE) xml = req.xml(MAX_ACL_BODY_SIZE)
if 'HTTP_X_AMZ_ACL' in req.environ and xml: if all(['HTTP_X_AMZ_ACL' in req.environ, xml]):
# S3 doesn't allow to give ACL with both ACL header and body. # S3 doesn't allow to give ACL with both ACL header and body.
raise UnexpectedContent() raise UnexpectedContent()
elif xml and 'HTTP_X_AMZ_ACL' not in req.environ: elif not any(['HTTP_X_AMZ_ACL' in req.environ, xml]):
# We very likely have an XML-based ACL request. # Both canned ACL header and xml body are missing
try: raise MissingSecurityHeader(missing_header_name='x-amz-acl')
translated_acl = swift_acl_translate(xml, xml=True) else:
except ACLError: # correct ACL exists in the request
raise MalformedACLError() if xml:
# We very likely have an XML-based ACL request.
# let's try to translate to the request header
try:
translated_acl = swift_acl_translate(xml, xml=True)
except ACLError:
raise MalformedACLError()
for header, acl in translated_acl: for header, acl in translated_acl:
req.headers[header] = acl req.headers[header] = acl
resp = req.get_response(self.app, 'POST') resp = req.get_response(self.app, 'POST')
resp.status = HTTP_OK resp.status = HTTP_OK

View File

@ -21,7 +21,8 @@ from swift3.controllers.base import Controller, bucket_operation
from swift3.etree import Element, SubElement, fromstring, tostring, \ from swift3.etree import Element, SubElement, fromstring, tostring, \
XMLSyntaxError, DocumentInvalid XMLSyntaxError, DocumentInvalid
from swift3.response import HTTPOk, S3NotImplemented, NoSuchKey, \ from swift3.response import HTTPOk, S3NotImplemented, NoSuchKey, \
ErrorResponse, MalformedXML, UserKeyMustBeSpecified, AccessDenied ErrorResponse, MalformedXML, UserKeyMustBeSpecified, AccessDenied, \
MissingRequestBodyError
from swift3.cfg import CONF from swift3.cfg import CONF
from swift3.utils import LOGGER from swift3.utils import LOGGER
@ -64,7 +65,11 @@ class MultiObjectDeleteController(Controller):
yield key, version yield key, version
try: try:
xml = req.xml(MAX_MULTI_DELETE_BODY_SIZE, check_md5=True) xml = req.xml(MAX_MULTI_DELETE_BODY_SIZE)
if not xml:
raise MissingRequestBodyError()
req.check_md5(xml)
elem = fromstring(xml, 'Delete') elem = fromstring(xml, 'Delete')
quiet = elem.find('./Quiet') quiet = elem.find('./Quiet')

View File

@ -558,6 +558,9 @@ class UploadController(Controller):
previous_number = 0 previous_number = 0
try: try:
xml = req.xml(MAX_COMPLETE_UPLOAD_BODY_SIZE) xml = req.xml(MAX_COMPLETE_UPLOAD_BODY_SIZE)
if not xml:
raise InvalidRequest(msg='You must specify at least one part')
complete_elem = fromstring(xml, 'CompleteMultipartUpload') complete_elem = fromstring(xml, 'CompleteMultipartUpload')
for part_elem in complete_elem.iterchildren('Part'): for part_elem in complete_elem.iterchildren('Part'):
part_number = int(part_elem.find('./PartNumber').text) part_number = int(part_elem.find('./PartNumber').text)

View File

@ -682,7 +682,7 @@ class Request(swob.Request):
""" """
raise AttributeError("No attribute 'body'") raise AttributeError("No attribute 'body'")
def xml(self, max_length, check_md5=False): def xml(self, max_length):
""" """
Similar to swob.Request.body, but it checks the content length before Similar to swob.Request.body, but it checks the content length before
creating a body string. creating a body string.
@ -697,11 +697,13 @@ class Request(swob.Request):
if self.message_length() > max_length: if self.message_length() > max_length:
raise MalformedXML() raise MalformedXML()
# Limit the read similar to how SLO handles manifests if te or self.message_length():
body = self.body_file.read(max_length) # Limit the read similar to how SLO handles manifests
body = self.body_file.read(max_length)
if check_md5: else:
self.check_md5(body) # No (or zero) Content-Length provided, and not chunked transfer;
# no body. Assume zero-length, and enforce a required body below.
return None
return body return body

View File

@ -159,3 +159,26 @@ class FakeSwift(object):
def clear_calls(self): def clear_calls(self):
del self._calls[:] del self._calls[:]
class UnreadableInput(object):
# Some clients will send neither a Content-Length nor a Transfer-Encoding
# header, which will cause (some versions of?) eventlet to bomb out on
# reads. This class helps us simulate that behavior.
def __init__(self, test_case):
self.calls = 0
self.test_case = test_case
def read(self, *a, **kw):
self.calls += 1
# Calling wsgi.input.read with neither a Content-Length nor
# a Transfer-Encoding header will raise TypeError (See
# https://bugs.launchpad.net/swift3/+bug/1593870 in detail)
# This unreadable class emulates the behavior
raise TypeError
def __enter__(self):
return self
def __exit__(self, *args):
self.test_case.assertEqual(0, self.calls)

View File

@ -15,10 +15,12 @@
import unittest import unittest
from cStringIO import StringIO from cStringIO import StringIO
from hashlib import md5
from swift.common.swob import Request, HTTPAccepted from swift.common.swob import Request, HTTPAccepted
from swift3.test.unit import Swift3TestCase from swift3.test.unit import Swift3TestCase
from swift3.test.unit.helpers import UnreadableInput
from swift3.etree import fromstring, tostring, Element, SubElement, XMLNS_XSI from swift3.etree import fromstring, tostring, Element, SubElement, XMLNS_XSI
from swift3.test.unit.test_s3_acl import s3acl from swift3.test.unit.test_s3_acl import s3acl
import mock import mock
@ -125,6 +127,40 @@ class TestSwift3Acl(Swift3TestCase):
self.assertEqual(self._get_error_code(body), self.assertEqual(self._get_error_code(body),
'UnexpectedContent') 'UnexpectedContent')
def _test_put_no_body(self, use_content_length=False,
use_transfer_encoding=False, string_to_md5=''):
content_md5 = md5(string_to_md5).digest().encode('base64').strip()
with UnreadableInput(self) as fake_input:
req = Request.blank(
'/bucket?acl',
environ={
'REQUEST_METHOD': 'PUT',
'wsgi.input': fake_input},
headers={
'Authorization': 'AWS test:tester:hmac',
'Date': self.get_date_header(),
'Content-MD5': content_md5},
body='')
if not use_content_length:
req.environ.pop('CONTENT_LENGTH')
if use_transfer_encoding:
req.environ['HTTP_TRANSFER_ENCODING'] = 'chunked'
status, headers, body = self.call_swift3(req)
self.assertEqual(status, '400 Bad Request')
self.assertEqual(self._get_error_code(body), 'MissingSecurityHeader')
self.assertEqual(self._get_error_message(body),
'Your request was missing a required header.')
self.assertIn('<MissingHeaderName>x-amz-acl</MissingHeaderName>', body)
@s3acl
def test_bucket_fails_with_neither_acl_header_nor_xml_PUT(self):
self._test_put_no_body()
self._test_put_no_body(string_to_md5='test')
self._test_put_no_body(use_content_length=True)
self._test_put_no_body(use_content_length=True, string_to_md5='test')
self._test_put_no_body(use_transfer_encoding=True)
self._test_put_no_body(use_transfer_encoding=True, string_to_md5='zz')
def test_object_acl_GET(self): def test_object_acl_GET(self):
req = Request.blank('/bucket/object?acl', req = Request.blank('/bucket/object?acl',
environ={'REQUEST_METHOD': 'GET'}, environ={'REQUEST_METHOD': 'GET'},

View File

@ -21,6 +21,7 @@ from swift.common.swob import Request
from swift.common.utils import json from swift.common.utils import json
from swift3.test.unit import Swift3TestCase from swift3.test.unit import Swift3TestCase
from swift3.test.unit.helpers import UnreadableInput
from swift3.etree import Element, SubElement, fromstring, tostring from swift3.etree import Element, SubElement, fromstring, tostring
from swift3.test.unit.test_s3_acl import s3acl from swift3.test.unit.test_s3_acl import s3acl
from swift3.subresource import Owner, encode_acl, ACLPublicRead from swift3.subresource import Owner, encode_acl, ACLPublicRead
@ -501,6 +502,7 @@ class TestSwift3Bucket(Swift3TestCase):
headers={'Authorization': 'AWS test:tester:hmac', headers={'Authorization': 'AWS test:tester:hmac',
'Date': self.get_date_header()}) 'Date': self.get_date_header()})
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
self.assertEqual(body, '')
self.assertEqual(status.split()[0], '200') self.assertEqual(status.split()[0], '200')
self.assertEqual(headers['Location'], '/bucket') self.assertEqual(headers['Location'], '/bucket')
@ -512,6 +514,19 @@ class TestSwift3Bucket(Swift3TestCase):
'Date': self.get_date_header(), 'Date': self.get_date_header(),
'Transfer-Encoding': 'chunked'}) 'Transfer-Encoding': 'chunked'})
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
self.assertEqual(body, '')
self.assertEqual(status.split()[0], '200')
self.assertEqual(headers['Location'], '/bucket')
with UnreadableInput(self) as fake_input:
req = Request.blank(
'/bucket',
environ={'REQUEST_METHOD': 'PUT',
'wsgi.input': fake_input},
headers={'Authorization': 'AWS test:tester:hmac',
'Date': self.get_date_header()})
status, headers, body = self.call_swift3(req)
self.assertEqual(body, '')
self.assertEqual(status.split()[0], '200') self.assertEqual(status.split()[0], '200')
self.assertEqual(headers['Location'], '/bucket') self.assertEqual(headers['Location'], '/bucket')

View File

@ -22,6 +22,7 @@ from swift.common import swob
from swift.common.swob import Request from swift.common.swob import Request
from swift3.test.unit import Swift3TestCase from swift3.test.unit import Swift3TestCase
from swift3.test.unit.helpers import UnreadableInput
from swift3.etree import fromstring, tostring, Element, SubElement from swift3.etree import fromstring, tostring, Element, SubElement
from swift3.cfg import CONF from swift3.cfg import CONF
from swift3.test.unit.test_s3_acl import s3acl from swift3.test.unit.test_s3_acl import s3acl
@ -77,11 +78,10 @@ class TestSwift3MultiDelete(Swift3TestCase):
req = Request.blank('/bucket?delete', req = Request.blank('/bucket?delete',
environ={'REQUEST_METHOD': 'POST'}, environ={'REQUEST_METHOD': 'POST'},
headers={'Authorization': 'AWS test:tester:hmac', headers={'Authorization': 'AWS test:tester:hmac',
'Content-Type': 'multipart/form-data',
'Date': self.get_date_header(), 'Date': self.get_date_header(),
'Content-MD5': content_md5}, 'Content-MD5': content_md5},
body=body) body=body)
req.date = datetime.now()
req.content_type = 'text/plain'
status, headers, body = self.call_swift3(req) status, headers, body = self.call_swift3(req)
self.assertEqual(status.split()[0], '200') self.assertEqual(status.split()[0], '200')
@ -249,5 +249,36 @@ class TestSwift3MultiDelete(Swift3TestCase):
elem = fromstring(body) elem = fromstring(body)
self.assertEqual(len(elem.findall('Deleted')), len(self.keys)) self.assertEqual(len(elem.findall('Deleted')), len(self.keys))
def _test_no_body(self, use_content_length=False,
use_transfer_encoding=False, string_to_md5=''):
content_md5 = md5(string_to_md5).digest().encode('base64').strip()
with UnreadableInput(self) as fake_input:
req = Request.blank(
'/bucket?delete',
environ={
'REQUEST_METHOD': 'POST',
'wsgi.input': fake_input},
headers={
'Authorization': 'AWS test:tester:hmac',
'Date': self.get_date_header(),
'Content-MD5': content_md5},
body='')
if not use_content_length:
req.environ.pop('CONTENT_LENGTH')
if use_transfer_encoding:
req.environ['HTTP_TRANSFER_ENCODING'] = 'chunked'
status, headers, body = self.call_swift3(req)
self.assertEqual(status, '400 Bad Request')
self.assertEqual(self._get_error_code(body), 'MissingRequestBodyError')
@s3acl
def test_object_multi_DELETE_empty_body(self):
self._test_no_body()
self._test_no_body(string_to_md5='test')
self._test_no_body(use_content_length=True)
self._test_no_body(use_content_length=True, string_to_md5='test')
self._test_no_body(use_transfer_encoding=True)
self._test_no_body(use_transfer_encoding=True, string_to_md5='test')
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -14,10 +14,11 @@
# limitations under the License. # limitations under the License.
import base64 import base64
from hashlib import md5
from mock import patch
import os import os
import time import time
import unittest import unittest
from mock import patch
from urllib import quote from urllib import quote
from swift.common import swob from swift.common import swob
@ -25,6 +26,7 @@ from swift.common.swob import Request
from swift.common.utils import json from swift.common.utils import json
from swift3.test.unit import Swift3TestCase from swift3.test.unit import Swift3TestCase
from swift3.test.unit.helpers import UnreadableInput
from swift3.etree import fromstring, tostring from swift3.etree import fromstring, tostring
from swift3.subresource import Owner, Grant, User, ACL, encode_acl, \ from swift3.subresource import Owner, Grant, User, ACL, encode_acl, \
decode_acl, ACLPublicRead decode_acl, ACLPublicRead
@ -1670,6 +1672,39 @@ class TestSwift3MultiUpload(Swift3TestCase):
self.assertEqual('bytes=0-9', put_headers['Range']) self.assertEqual('bytes=0-9', put_headers['Range'])
self.assertEqual('/src_bucket/src_obj', put_headers['X-Copy-From']) self.assertEqual('/src_bucket/src_obj', put_headers['X-Copy-From'])
def _test_no_body(self, use_content_length=False,
use_transfer_encoding=False, string_to_md5=''):
content_md5 = md5(string_to_md5).digest().encode('base64').strip()
with UnreadableInput(self) as fake_input:
req = Request.blank(
'/bucket/object?uploadId=X',
environ={
'REQUEST_METHOD': 'POST',
'wsgi.input': fake_input},
headers={
'Authorization': 'AWS test:tester:hmac',
'Date': self.get_date_header(),
'Content-MD5': content_md5},
body='')
if not use_content_length:
req.environ.pop('CONTENT_LENGTH')
if use_transfer_encoding:
req.environ['HTTP_TRANSFER_ENCODING'] = 'chunked'
status, headers, body = self.call_swift3(req)
self.assertEqual(status, '400 Bad Request')
self.assertEqual(self._get_error_code(body), 'InvalidRequest')
self.assertEqual(self._get_error_message(body),
'You must specify at least one part')
@s3acl
def test_object_multi_upload_empty_body(self):
self._test_no_body()
self._test_no_body(string_to_md5='test')
self._test_no_body(use_content_length=True)
self._test_no_body(use_content_length=True, string_to_md5='test')
self._test_no_body(use_transfer_encoding=True)
self._test_no_body(use_transfer_encoding=True, string_to_md5='test')
class TestSwift3MultiUploadNonUTC(TestSwift3MultiUpload): class TestSwift3MultiUploadNonUTC(TestSwift3MultiUpload):
def setUp(self): def setUp(self):