Merge "Add labeled metrics to s3api"

This commit is contained in:
Zuul
2025-12-18 19:13:01 +00:00
committed by Gerrit Code Review
6 changed files with 729 additions and 20 deletions

View File

@@ -145,27 +145,61 @@ import json
from paste.deploy import loadwsgi from paste.deploy import loadwsgi
from urllib.parse import parse_qs from urllib.parse import parse_qs
from swift.common import swob
from swift.common.constraints import valid_api_version from swift.common.constraints import valid_api_version
from swift.common.middleware.listing_formats import \ from swift.common.middleware.listing_formats import \
MAX_CONTAINER_LISTING_CONTENT_LENGTH MAX_CONTAINER_LISTING_CONTENT_LENGTH
from swift.common.request_helpers import append_log_info from swift.common.request_helpers import append_log_info
from swift.common.wsgi import PipelineWrapper, loadcontext, WSGIContext from swift.common.wsgi import PipelineWrapper, loadcontext, WSGIContext
from swift.common.statsd_client import get_labeled_statsd_client
from swift.common.middleware import app_property from swift.common.middleware import app_property
from swift.common.middleware.s3api.exception import NotS3Request, \ from swift.common.middleware.s3api.exception import NotS3Request, \
InvalidSubresource InvalidSubresource
from swift.common.middleware.s3api.s3request import get_request_class from swift.common.middleware.s3api import s3request
from swift.common.middleware.s3api.s3response import ErrorResponse, \ from swift.common.middleware.s3api.s3response import ErrorResponse, \
InternalError, MethodNotAllowed, S3ResponseBase, S3NotImplemented InternalError, MethodNotAllowed, S3ResponseBase, S3NotImplemented
from swift.common.utils import get_logger, config_true_value, \ from swift.common.utils import get_logger, config_true_value, \
config_positive_int_value, split_path, closing_if_possible, \ config_positive_int_value, split_path, closing_if_possible, \
list_from_csv, parse_header, checksum list_from_csv, parse_header, checksum
from swift.common.middleware.s3api.utils import Config from swift.common.middleware.s3api.utils import Config, \
classify_checksum_header_value, make_header_label
from swift.common.middleware.s3api.acl_handlers import get_acl_handler from swift.common.middleware.s3api.acl_handlers import get_acl_handler
from swift.common.registry import register_swift_info, \ from swift.common.registry import register_swift_info, \
register_sensitive_header, register_sensitive_param register_sensitive_header, register_sensitive_param
# https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html
WELL_KNOWN_SPECIFIC_SHA256_VALUES = (
'UNSIGNED-PAYLOAD',
'STREAMING-UNSIGNED-PAYLOAD-TRAILER',
'STREAMING-AWS4-HMAC-SHA256-PAYLOAD',
'STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER',
'STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD',
'STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER'
)
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html
# https://docs.aws.amazon.com/AmazonS3/latest/API/API_Object.html#AmazonS3-Type-Object-ChecksumAlgorithm
# https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html
# docs are unclear whether the header value is the (un-)hyphenated form
# algorithms for x-amz-checksum-algorithm/ x-amz-sdk-checksum-algorithm
WELL_KNOWN_CHECKSUM_ALGORITHMS = (
'CRC64NVME',
'CRC32',
'CRC32C',
'SHA1',
'SHA256'
)
WELL_KNOWN_CHECKSUM_HEADERS = (
'x-amz-checksum-crc32',
'x-amz-checksum-crc32c',
'x-amz-checksum-sha1',
'x-amz-checksum-sha256',
'x-amz-checksum-crc64nvme'
)
class ListingEtagMiddleware(object): class ListingEtagMiddleware(object):
def __init__(self, app): def __init__(self, app):
self.app = app self.app = app
@@ -299,6 +333,8 @@ class S3ApiMiddleware(object):
self.logger = get_logger( self.logger = get_logger(
wsgi_conf, log_route='s3api', statsd_tail_prefix='s3api') wsgi_conf, log_route='s3api', statsd_tail_prefix='s3api')
self.statsd = get_labeled_statsd_client(wsgi_conf, self.logger)
self.check_pipeline(wsgi_conf) self.check_pipeline(wsgi_conf)
checksum.log_selected_implementation(self.logger) checksum.log_selected_implementation(self.logger)
@@ -316,7 +352,78 @@ class S3ApiMiddleware(object):
# Not S3, apparently # Not S3, apparently
return False return False
def _make_req_header_labels(self, env):
req_headers = swob.HeaderEnvironProxy(env)
labels = {}
for hdr_key, hdr_val in req_headers.items():
label_val = None
hdr_key = hdr_key.lower()
label_key = make_header_label(hdr_key)
if hdr_key == 'content-encoding':
if 'aws-chunked' in list_from_csv(hdr_val.lower()):
label_val = 'aws-chunked'
elif hdr_key == 'transfer-encoding':
if 'chunked' in list_from_csv(hdr_val.lower()):
label_val = 'chunked'
elif hdr_key == 'x-amz-decoded-content-length':
label_val = True
elif hdr_key == 'x-amz-content-sha256':
if hdr_val in WELL_KNOWN_SPECIFIC_SHA256_VALUES:
label_val = hdr_val
else:
label_val = classify_checksum_header_value(hdr_val)
elif hdr_key == 'content-md5':
label_val = classify_checksum_header_value(hdr_val)
elif hdr_key in s3request.CHECKSUMS_BY_HEADER.keys():
label_val = classify_checksum_header_value(hdr_val)
elif hdr_key == 'x-amz-trailer':
if hdr_val.lower() in s3request.CHECKSUMS_BY_HEADER.keys():
label_val = hdr_val.lower()
else:
label_val = 'unknown'
elif hdr_key in ('x-amz-checksum-algorithm',
'x-amz-sdk-checksum-algorithm'):
hdr_val_normalised = hdr_val.upper().replace('-', '')
if hdr_val_normalised in WELL_KNOWN_CHECKSUM_ALGORITHMS:
label_val = hdr_val_normalised
else:
label_val = 'unknown'
if label_val is not None:
labels[label_key] = label_val
return labels
def _emit_response_header_stats(self, env, resp, labels):
if not labels:
return
labels['status'] = resp.status_int
labels['method'] = env.get('REQUEST_METHOD')
swift_path = env.get('swift.backend_path')
if swift_path:
vers, acc, con, obj = split_path(swift_path, 1, 4, True)
if obj:
labels['type'] = 'object'
labels['account'] = acc
labels['container'] = con
elif con:
labels['type'] = 'container'
labels['account'] = acc
labels['container'] = con
elif acc:
labels['account'] = acc
labels['type'] = 'account'
else:
labels['type'] = 'UNKNOWN'
else:
labels['type'] = 'UNKNOWN'
self.statsd.increment("swift_s3_checksum_algo_request", labels=labels)
def __call__(self, env, start_response): def __call__(self, env, start_response):
# get metrics header labels before any mutation of the headers
req_header_labels = self._make_req_header_labels(env)
origin = env.get('HTTP_ORIGIN') origin = env.get('HTTP_ORIGIN')
if self.conf.cors_preflight_allow_origin and \ if self.conf.cors_preflight_allow_origin and \
self.is_s3_cors_preflight(env): self.is_s3_cors_preflight(env):
@@ -346,11 +453,11 @@ class S3ApiMiddleware(object):
return [b''] return [b'']
try: try:
req_class = get_request_class(env, self.conf.s3_acl) req_class = s3request.get_request_class(env, self.conf.s3_acl)
req = req_class(env, self.app, self.conf) req = req_class(env, self.app, self.conf)
resp = self.handle_request(req) resp = self.handle_request(req)
except NotS3Request: except NotS3Request:
resp = self.app return self.app(env, start_response)
except InvalidSubresource as e: except InvalidSubresource as e:
self.logger.debug(e.cause) self.logger.debug(e.cause)
except ErrorResponse as err_resp: except ErrorResponse as err_resp:
@@ -370,6 +477,9 @@ class S3ApiMiddleware(object):
if 's3api.backend_path' in env and 'swift.backend_path' not in env: if 's3api.backend_path' in env and 'swift.backend_path' not in env:
env['swift.backend_path'] = env['s3api.backend_path'] env['swift.backend_path'] = env['s3api.backend_path']
# emit metric with header labels now path and status may be available
self._emit_response_header_stats(env, resp, req_header_labels)
return resp(env, start_response) return resp(env, start_response)
def handle_request(self, req): def handle_request(self, req):

View File

@@ -55,6 +55,10 @@ def snake_to_camel(snake):
return snake.title().replace('_', '') return snake.title().replace('_', '')
def make_header_label(header):
return 'header_' + header.lower().replace('-', '_')
def unique_id(): def unique_id():
result = base64.urlsafe_b64encode(str(uuid.uuid4()).encode('ascii')) result = base64.urlsafe_b64encode(str(uuid.uuid4()).encode('ascii'))
return result.decode('ascii') return result.decode('ascii')
@@ -72,6 +76,37 @@ def utf8decode(s):
return s return s
def is_valid_base64(s):
try:
base64.b64decode(s)
return True
except Exception:
return False
def is_valid_hash(hash_string):
try:
int(hash_string, 16)
except ValueError:
return False
return True
def classify_checksum_header_value(value):
if is_valid_hash(value):
if len(value) in (8, 16, 20, 32, 64, 128, 256, 512):
return 'hash_%d' % len(value)
elif is_valid_base64(value):
# crc32 -> b64_8
# crc64 -> b64_12
# md5 -> b64_24
# sha1 -> b64_28
# sha256 -> b64_44
if len(value) in (8, 12, 24, 28, 44):
return 'b64_%d' % len(value)
return 'unknown'
def validate_bucket_name(name, dns_compliant_bucket_names): def validate_bucket_name(name, dns_compliant_bucket_names):
""" """
Validates the name of the bucket against S3 criteria, Validates the name of the bucket against S3 criteria,

View File

@@ -187,6 +187,7 @@ SWIFT_CONF_FILE = '/etc/swift/swift.conf'
O_TMPFILE = getattr(os, 'O_TMPFILE', 0o20000000 | os.O_DIRECTORY) O_TMPFILE = getattr(os, 'O_TMPFILE', 0o20000000 | os.O_DIRECTORY)
MD5_OF_EMPTY_STRING = 'd41d8cd98f00b204e9800998ecf8427e' MD5_OF_EMPTY_STRING = 'd41d8cd98f00b204e9800998ecf8427e'
RESERVED_BYTE = b'\x00' RESERVED_BYTE = b'\x00'
RESERVED_STR = u'\x00' RESERVED_STR = u'\x00'
RESERVED = '\x00' RESERVED = '\x00'

View File

@@ -30,6 +30,7 @@ from swift.common.middleware.s3api.subresource import Owner, encode_acl, \
Grant, User, ACL, PERMISSIONS, AllUsers, AuthenticatedUsers Grant, User, ACL, PERMISSIONS, AllUsers, AuthenticatedUsers
from test.unit.common.middleware.helpers import FakeSwift from test.unit.common.middleware.helpers import FakeSwift
from test.debug_logger import FakeLabeledStatsdClient
class FakeAuthApp(object): class FakeAuthApp(object):
@@ -118,8 +119,11 @@ class S3ApiTestCase(unittest.TestCase):
self.swift = FakeSwift() self.swift = FakeSwift()
self.app = self._wrap_app(self.swift) self.app = self._wrap_app(self.swift)
self.app._pipeline_final_app = self.swift self.app._pipeline_final_app = self.swift
with mock.patch('swift.common.statsd_client.LabeledStatsdClient',
FakeLabeledStatsdClient):
self.s3api = filter_factory({}, **self.conf)(self.app) self.s3api = filter_factory({}, **self.conf)(self.app)
self.logger = self.s3api.logger = self.swift.logger self.logger = self.s3api.logger = self.swift.logger
self.statsd = self.s3api.statsd
# if you change the registered acl response for /bucket or # if you change the registered acl response for /bucket or
# /bucket/object tearDown will complain at you; you can set this to # /bucket/object tearDown will complain at you; you can set this to

View File

@@ -15,6 +15,7 @@
# limitations under the License. # limitations under the License.
import base64 import base64
import hashlib
import io import io
import unittest import unittest
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
@@ -41,7 +42,8 @@ from swift.common.utils import md5, get_logger
from keystonemiddleware.auth_token import AuthProtocol from keystonemiddleware.auth_token import AuthProtocol
from keystoneauth1.access import AccessInfoV2 from keystoneauth1.access import AccessInfoV2
from test.debug_logger import debug_logger, FakeStatsdClient from test.debug_logger import debug_logger, FakeStatsdClient, \
FakeLabeledStatsdClient
from test.unit.common.middleware.s3api import S3ApiTestCase from test.unit.common.middleware.s3api import S3ApiTestCase
from test.unit.common.middleware.helpers import FakeSwift from test.unit.common.middleware.helpers import FakeSwift
from test.unit.common.middleware.s3api.test_s3token import \ from test.unit.common.middleware.s3api.test_s3token import \
@@ -52,6 +54,9 @@ from swift.common.middleware.s3api.s3api import filter_factory, \
S3ApiMiddleware S3ApiMiddleware
from swift.common.middleware.s3api.s3token import S3Token from swift.common.middleware.s3api.s3token import S3Token
SHA256_OF_EMPTY_STRING = \
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
class TestListingMiddleware(S3ApiTestCase): class TestListingMiddleware(S3ApiTestCase):
def test_s3_etag_in_json(self): def test_s3_etag_in_json(self):
@@ -255,14 +260,42 @@ class TestS3ApiMiddleware(S3ApiTestCase):
mock_crc64nvme.__name__ = 'crc64nvme_isal' mock_crc64nvme.__name__ = 'crc64nvme_isal'
S3ApiMiddleware(None, {}) S3ApiMiddleware(None, {})
self.assertEqual( self.assertEqual(
{'info': ['Using crc32c_isal implementation for CRC32C.', {
'Using crc64nvme_isal implementation for CRC64NVME.']}, 'debug': [
'Labeled statsd mode: disabled (fake-swift)',
],
'info': [
'Using crc32c_isal implementation for CRC32C.',
'Using crc64nvme_isal implementation for CRC64NVME.',
],
},
self.logger.all_log_lines()) self.logger.all_log_lines())
def test_init_statsd_options_user_labels(self):
conf = {
'log_statsd_host': 'example.com',
'log_statsd_port': '1234',
'statsd_label_mode': 'dogstatsd',
'statsd_emit_legacy': False,
'statsd_user_label_userdefined': 'whatever',
}
with mock.patch('swift.common.statsd_client.LabeledStatsdClient',
FakeLabeledStatsdClient):
s3api = S3ApiMiddleware(None, conf)
statsd = s3api.statsd
self.assertIsInstance(statsd, FakeLabeledStatsdClient)
statsd.increment('baz', labels={'label_foo': 'foo'})
self.assertEqual(
[(b'baz:1|c|#label_foo:foo,user_userdefined:whatever',
('example.com', 1234))],
statsd.sendto_calls)
def test_non_s3_request_passthrough(self): def test_non_s3_request_passthrough(self):
req = Request.blank('/something') req = Request.blank('/something')
status, headers, body = self.call_s3api(req) status, headers, body = self.call_s3api(req)
self.assertEqual(body, b'FAKE APP') self.assertEqual(body, b'FAKE APP')
self.assertFalse(self.statsd.calls['increment'])
def test_bad_format_authorization(self): def test_bad_format_authorization(self):
req = Request.blank('/something', req = Request.blank('/something',
@@ -699,30 +732,52 @@ class TestS3ApiMiddleware(S3ApiTestCase):
get_log_info(req.environ)) get_log_info(req.environ))
def test_bucket_virtual_hosted_style(self): def test_bucket_virtual_hosted_style(self):
req = Request.blank('/', req = Request.blank(
'/',
environ={'HTTP_HOST': 'bucket.localhost:80', environ={'HTTP_HOST': 'bucket.localhost:80',
'REQUEST_METHOD': 'HEAD', 'REQUEST_METHOD': 'HEAD',
'HTTP_AUTHORIZATION': 'HTTP_AUTHORIZATION':
'AWS test:tester:hmac'}, 'AWS test:tester:hmac'},
headers={'Date': self.get_date_header()}) headers={'Date': self.get_date_header(),
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING})
status, headers, body = self.call_s3api(req) status, headers, body = self.call_s3api(req)
self.assertEqual(status.split()[0], '200') self.assertEqual(status.split()[0], '200')
self.assertIn('swift.backend_path', req.environ) self.assertIn('swift.backend_path', req.environ)
self.assertEqual('/v1/AUTH_test/bucket', self.assertEqual('/v1/AUTH_test/bucket',
req.environ['swift.backend_path']) req.environ['swift.backend_path'])
exp_labels = {'account': 'AUTH_test',
'container': 'bucket',
'method': 'HEAD',
'type': 'container',
'status': 200,
'header_x_amz_content_sha256': 'hash_64'}
self.assertEqual([(('swift_s3_checksum_algo_request',),
{'labels': exp_labels})],
self.statsd.calls['increment'])
def test_object_virtual_hosted_style(self): def test_object_virtual_hosted_style(self):
req = Request.blank('/object', req = Request.blank(
'/object',
environ={'HTTP_HOST': 'bucket.localhost:80', environ={'HTTP_HOST': 'bucket.localhost:80',
'REQUEST_METHOD': 'HEAD', 'REQUEST_METHOD': 'HEAD',
'HTTP_AUTHORIZATION': 'HTTP_AUTHORIZATION':
'AWS test:tester:hmac'}, 'AWS test:tester:hmac'},
headers={'Date': self.get_date_header()}) headers={'Date': self.get_date_header(),
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING})
status, headers, body = self.call_s3api(req) status, headers, body = self.call_s3api(req)
self.assertEqual(status.split()[0], '200') self.assertEqual(status.split()[0], '200')
self.assertIn('swift.backend_path', req.environ) self.assertIn('swift.backend_path', req.environ)
self.assertEqual('/v1/AUTH_test/bucket/object', self.assertEqual('/v1/AUTH_test/bucket/object',
req.environ['swift.backend_path']) req.environ['swift.backend_path'])
exp_labels = {'account': 'AUTH_test',
'container': 'bucket',
'method': 'HEAD',
'type': 'object',
'status': 200,
'header_x_amz_content_sha256': 'hash_64'}
self.assertEqual([(('swift_s3_checksum_algo_request',),
{'labels': exp_labels})],
self.statsd.calls['increment'])
def test_token_generation(self): def test_token_generation(self):
self.swift.register('HEAD', '/v1/AUTH_test/bucket+segments/' self.swift.register('HEAD', '/v1/AUTH_test/bucket+segments/'
@@ -1790,6 +1845,492 @@ class TestS3ApiMiddleware(S3ApiTestCase):
['HEAD /bucket/object s3:err:AccessDenied.invalid_credential'], ['HEAD /bucket/object s3:err:AccessDenied.invalid_credential'],
self.logger.get_lines_for_level('info')) self.logger.get_lines_for_level('info'))
def _do_test_emit_header_stats(self, extra_headers,
method='PUT',
path='/bucket/object'):
authz_header = 'AWS4-HMAC-SHA256 ' + ', '.join([
'Credential=test:tester/%s/us-east-1/s3/aws4_request' %
self.get_v4_amz_date_header().split('T', 1)[0],
'SignedHeaders=host;x-amz-date',
'Signature=X',
])
headers = {
'Authorization': authz_header,
'X-Amz-Date': self.get_v4_amz_date_header(),
'Content-Type': 'text/plain',
'Content-Length': '0',
}
headers.update(extra_headers)
req = Request.blank(path, headers=headers, body='')
req.method = method
self.statsd.clear()
# verify that request headers are sampled before request is handled by
# mocking the controller to mutate the request headers
orig_get_response = S3Request.get_response
captured_envs = []
def mock_handler(req, *args, **kwargs):
# note: only requests that succeed in constructing an S3Request
# will reach this handler
captured_envs.append(req)
resp = orig_get_response(req, *args, **kwargs)
for k in extra_headers:
req.headers.pop(k, None)
return resp
with mock.patch('swift.common.middleware.s3api.s3request.S3Request.'
'get_response', mock_handler):
_, _, body = self.call_s3api(req)
self.assertEqual([(('swift_s3_checksum_algo_request',), mock.ANY)],
self.statsd.calls['increment'])
kwargs = self.statsd.calls['increment'][0][1]
self.assertIn('labels', kwargs)
return kwargs['labels']
def test_emit_stats_x_amx_content_sha256_real_hash(self):
headers = {'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'method': 'PUT',
'type': 'object',
'status': 200,
'header_x_amz_content_sha256': 'hash_64'},
labels)
def test_emit_stats_x_amx_content_sha256_real_hash_GET(self):
# boto3 sends this header with GETs...
headers = {'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
resp_body = json.dumps([]).encode('ascii')
self.swift.register('GET', '/v1/AUTH_test', swob.HTTPOk, {}, resp_body)
labels = self._do_test_emit_header_stats(headers,
method='GET',
path='/')
self.assertEqual({'account': 'AUTH_test',
'method': 'GET',
'type': 'account',
'status': 200,
'header_x_amz_content_sha256': 'hash_64'},
labels)
self.swift.register('GET', '/v1/AUTH_test/bucket', swob.HTTPOk, {},
resp_body)
labels = self._do_test_emit_header_stats(headers,
method='GET',
path='/bucket')
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'method': 'GET',
'type': 'container',
'status': 200,
'header_x_amz_content_sha256': 'hash_64'},
labels)
labels = self._do_test_emit_header_stats(headers,
method='GET',
path='/bucket/object')
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'method': 'GET',
'type': 'object',
'status': 200,
'header_x_amz_content_sha256': 'hash_64'},
labels)
self.swift.register('GET', '/v1/AUTH_test/bucket/object',
swob.HTTPNotFound, {}, "")
labels = self._do_test_emit_header_stats(headers,
method='GET',
path='/bucket/object')
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'method': 'GET',
'type': 'object',
'status': 404,
'header_x_amz_content_sha256': 'hash_64'},
labels)
def test_emit_stats_x_amx_checksum_sha256_real_hash(self):
headers = {'X-Amz-Checksum-SHA256': base64.b64encode(
hashlib.sha256().digest())}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'method': 'PUT',
'type': 'UNKNOWN',
'status': 400,
'header_x_amz_checksum_sha256': 'b64_44'},
labels)
def test_emit_stats_x_amx_content_sha256_supported_aliases(self):
def do_test(alias):
headers = {'X-Amz-Content-SHA256': alias}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'method': 'PUT',
'type': 'object',
'status': 200,
'header_x_amz_content_sha256': alias},
labels)
do_test('UNSIGNED-PAYLOAD')
def test_emit_stats_x_amx_content_sha256_supported_streaming_aliases(self):
def do_test(alias):
headers = {'X-Amz-Content-SHA256': alias,
'x-amz-decoded-content-length': '0'}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'method': 'PUT',
'type': 'object',
'status': 400, # incomplete payload
'header_x_amz_decoded_content_length': True,
'header_x_amz_content_sha256': alias},
labels)
do_test('STREAMING-UNSIGNED-PAYLOAD-TRAILER')
do_test('STREAMING-AWS4-HMAC-SHA256-PAYLOAD')
do_test('STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER')
def test_emit_stats_x_amx_content_sha256_unsupported_aliases(self):
def do_test(alias):
headers = {'X-Amz-Content-SHA256': alias,
'x-amz-decoded-content-length': '0'}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'method': 'PUT',
'type': 'UNKNOWN',
'status': 501,
'header_x_amz_decoded_content_length': True,
'header_x_amz_content_sha256': alias},
labels)
do_test('STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD')
do_test('STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER')
def test_emit_stats_x_amx_content_sha256_invalid(self):
def do_test(value):
headers = {'X-Amz-Content-SHA256': value}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'method': 'PUT',
'type': 'UNKNOWN',
'status': 400,
'header_x_amz_content_sha256': 'unknown'},
labels)
do_test('0' * 63)
do_test('UNSIGNED-NONSENSE')
def test_emit_stats_content_md5(self):
headers = {'Content-MD5': base64.b64encode(md5(b'').digest()),
# X-Amz-Content-SHA256 is required
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'method': 'PUT',
'type': 'object',
'status': 200,
'header_content_md5': 'b64_24',
'header_x_amz_content_sha256': 'hash_64'},
labels)
headers = {'Content-MD5': 'nonsense',
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'method': 'PUT',
'type': 'UNKNOWN',
'status': 400,
'header_content_md5': 'b64_8',
'header_x_amz_content_sha256': 'hash_64'},
labels)
def test_emit_stats_content_encoding(self):
headers = {'Content-Encoding': 'aws-chunked',
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'method': 'PUT',
'type': 'object',
'status': 200,
'header_content_encoding': 'aws-chunked',
'header_x_amz_content_sha256': 'hash_64'},
labels)
headers = {'Content-Encoding': 'aws-chunked,gzip',
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'method': 'PUT',
'type': 'object',
'status': 200,
'header_content_encoding': 'aws-chunked',
'header_x_amz_content_sha256': 'hash_64'},
labels)
# s3api sees 'aws-chunked' in 'not-aws-chunked' and treats the
# request as unsupported rather than ignoring 'not-aws-chunked' !
headers = {'Content-Encoding': 'not-aws-chunked',
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'method': 'PUT',
'type': 'object',
'status': 200,
'header_x_amz_content_sha256': 'hash_64'},
labels)
def test_emit_stats_transfer_encoding(self):
headers = {'Transfer-Encoding': 'chunked',
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'type': 'object',
'method': 'PUT',
'status': 200,
'header_transfer_encoding': 'chunked',
'header_x_amz_content_sha256': 'hash_64'},
labels)
headers = {'Transfer-Encoding': 'chunked,gzip',
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'type': 'object',
'method': 'PUT',
'status': 200,
'header_transfer_encoding': 'chunked',
'header_x_amz_content_sha256': 'hash_64'},
labels)
headers = {'Transfer-Encoding': 'aws-chunked',
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'type': 'object',
'method': 'PUT',
'status': 200,
'header_x_amz_content_sha256': 'hash_64'},
labels)
def test_emit_stats_x_amz_decoded_content_length(self):
headers = {'X-Amz-Decoded-Content-Length': '123',
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'method': 'PUT',
'type': 'object',
'status': 200,
'header_x_amz_decoded_content_length': True,
'header_x_amz_content_sha256': 'hash_64'},
labels)
def test_emit_stats_x_amz_checksum_crc32(self):
headers = {'X-Amz-Checksum-Crc32': base64.b64encode(b'1234'),
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'method': 'PUT',
'type': 'object',
'status': 400, # bad digest
'header_x_amz_checksum_crc32': 'b64_8',
'header_x_amz_content_sha256': 'hash_64'},
labels)
headers = {'X-Amz-Checksum-Crc32': base64.b64encode(b'123'), # bad
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'method': 'PUT',
'type': 'UNKNOWN',
'status': 400,
'header_x_amz_checksum_crc32': 'unknown',
'header_x_amz_content_sha256': 'hash_64'},
labels)
def test_emit_stats_x_amz_checksum_crc32c(self):
headers = {'X-Amz-Checksum-Crc32c': base64.b64encode(b'1234'),
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'method': 'PUT',
'type': 'object',
'status': 400, # bad digest
'header_x_amz_checksum_crc32c': 'b64_8',
'header_x_amz_content_sha256': 'hash_64'},
labels)
headers = {'X-Amz-Checksum-Crc32c': base64.b64encode(b'123'), # bad
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'method': 'PUT',
'type': 'UNKNOWN',
'status': 400,
'header_x_amz_checksum_crc32c': 'unknown',
'header_x_amz_content_sha256': 'hash_64'},
labels)
def test_emit_stats_x_amz_checksum_crc64nvme(self):
headers = {'X-Amz-Checksum-Crc32c': base64.b64encode(b'12345678'),
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'type': 'UNKNOWN',
'method': 'PUT',
'status': 400, # bad digest
'header_x_amz_checksum_crc32c': 'b64_12',
'header_x_amz_content_sha256': 'hash_64'},
labels)
def test_emit_stats_x_amz_checksum_sha1(self):
headers = {'X-Amz-Checksum-SHA1': base64.b64encode(b'1234' * 5),
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'method': 'PUT',
'type': 'object',
'status': 400, # bad digest
'header_x_amz_checksum_sha1': 'b64_28',
'header_x_amz_content_sha256': 'hash_64'},
labels)
headers = {'X-Amz-Checksum-SHA1': base64.b64encode(b'123' * 5), # bad
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'method': 'PUT',
'type': 'UNKNOWN',
'status': 400, # invalid header value
'header_x_amz_checksum_sha1': 'unknown',
'header_x_amz_content_sha256': 'hash_64'},
labels)
def test_emit_stats_multiple_x_amz_checksums(self):
headers = {'X-Amz-Checksum-SHA1': base64.b64encode(b'1234' * 5),
'X-Amz-Checksum-CRC32': base64.b64encode(b'1234'),
'X-Amz-Content-SHA256': SHA256_OF_EMPTY_STRING}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'type': 'UNKNOWN',
'status': 400,
'method': 'PUT',
'header_x_amz_checksum_crc32': 'b64_8',
'header_x_amz_checksum_sha1': 'b64_28',
'header_x_amz_content_sha256': 'hash_64'},
labels)
def test_emit_stats_x_amz_trailer_unknown(self):
def do_test(header_value):
headers = {
'X-Amz-Trailer': header_value,
'X-Amz-Content-SHA256': 'STREAMING-UNSIGNED-PAYLOAD-TRAILER',
'x-amz-decoded-content-length': '0'
}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'method': 'PUT',
'type': 'UNKNOWN',
'status': 400,
'header_x_amz_decoded_content_length': True,
'header_x_amz_trailer': 'unknown',
'header_x_amz_content_sha256':
'STREAMING-UNSIGNED-PAYLOAD-TRAILER'},
labels)
do_test('content-md5')
do_test('x-amz-checksum-sha2')
do_test('content-md5,x-amz-checksum-sha256')
do_test('x-amz-checksum-sha256,x-amz-checksum-crc32')
def test_emit_stats_x_amz_trailer(self):
def do_test(header_value):
headers = {
'X-Amz-Trailer': header_value,
'X-Amz-Content-SHA256': 'STREAMING-UNSIGNED-PAYLOAD-TRAILER',
'x-amz-decoded-content-length': '0'
}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'account': 'AUTH_test',
'container': 'bucket',
'method': 'PUT',
'type': 'object',
'status': 400, # IncompleteBody
'header_x_amz_decoded_content_length': True,
'header_x_amz_trailer': header_value,
'header_x_amz_content_sha256':
'STREAMING-UNSIGNED-PAYLOAD-TRAILER'},
labels)
do_test('x-amz-checksum-crc32')
do_test('x-amz-checksum-crc32c')
with mock.patch('swift.common.utils.checksum.crc64nvme_isal') \
as mock_crc64nvme:
mock_crc64nvme.__name__ = 'crc64nvme_isal'
do_test('x-amz-checksum-crc64nvme')
do_test('x-amz-checksum-sha1')
do_test('x-amz-checksum-sha256')
def test_emit_stats_x_amz_sdk_checksum_algorithm(self):
def do_test(algo):
headers = {
'x-amz-sdk-checksum-algorithm': algo,
}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'method': 'PUT',
'type': 'UNKNOWN',
'status': 400,
'header_x_amz_sdk_checksum_algorithm':
algo.replace('-', '')},
labels)
do_test('CRC32')
do_test('CRC32C')
do_test('CRC64NVME')
do_test('SHA1')
do_test('SHA256')
do_test('CRC-32')
do_test('CRC-32C')
do_test('CRC-64NVME')
do_test('SHA-1')
do_test('SHA-256')
def test_emit_stats_x_amz_checksum_algorithm(self):
def do_test(algo):
headers = {
'X-Amz-Checksum-Algorithm': algo,
}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'method': 'PUT',
'type': 'UNKNOWN',
'status': 400,
'header_x_amz_checksum_algorithm':
algo.replace('-', '')},
labels)
do_test('CRC32')
do_test('CRC32C')
do_test('CRC64NVME')
do_test('SHA1')
do_test('SHA256')
do_test('CRC-32')
do_test('CRC-32C')
do_test('CRC-64NVME')
do_test('SHA-1')
do_test('SHA-256')
def test_emit_stats_x_amz_checksum_algorithm_unknown(self):
headers = {
'X-Amz-Checksum-Algorithm': 'CRC128',
}
labels = self._do_test_emit_header_stats(headers)
self.assertEqual({'method': 'PUT',
'type': 'UNKNOWN',
'status': 400,
'header_x_amz_checksum_algorithm': 'unknown'},
labels)
def test_access_user_id_logging(self): def test_access_user_id_logging(self):
# verify that proxy logging gets access_user_id from S3 requests # verify that proxy logging gets access_user_id from S3 requests
environ = {'REQUEST_METHOD': 'GET'} environ = {'REQUEST_METHOD': 'GET'}

View File

@@ -20,6 +20,7 @@ import unittest
from swift.common.swob import Request from swift.common.swob import Request
from swift.common.middleware.s3api import utils, s3request from swift.common.middleware.s3api import utils, s3request
from swift.common.middleware.s3api.exception import InvalidBucketNameParseError from swift.common.middleware.s3api.exception import InvalidBucketNameParseError
from swift.common.middleware.s3api.utils import make_header_label
strs = [ strs = [
('Owner', 'owner'), ('Owner', 'owner'),
@@ -37,6 +38,23 @@ class TestS3ApiUtils(unittest.TestCase):
for s1, s2 in strs: for s1, s2 in strs:
self.assertEqual(s1, utils.snake_to_camel(s2)) self.assertEqual(s1, utils.snake_to_camel(s2))
def test_make_header_label(self):
self.assertEqual('header_aa_b_c', make_header_label('Aa-B-C'))
self.assertEqual('header_aa_b_c', make_header_label('AA_B_C'))
self.assertEqual('header_aa_b_c', make_header_label('aA-b-c'))
def test_classify_checksum_header_value(self):
self.assertEqual(
utils.classify_checksum_header_value('00000000'), 'hash_8')
self.assertEqual(
utils.classify_checksum_header_value('a' * 64), 'hash_64')
self.assertEqual(
utils.classify_checksum_header_value('STUVWXYZ'), 'b64_8')
self.assertEqual(
utils.classify_checksum_header_value('abcdef&1'), 'unknown')
self.assertEqual(
utils.classify_checksum_header_value('z'), 'unknown')
def test_validate_bucket_name(self): def test_validate_bucket_name(self):
# good cases # good cases
self.assertTrue(utils.validate_bucket_name('bucket', True)) self.assertTrue(utils.validate_bucket_name('bucket', True))