Add normalize_etag() helper function

... and drive-by a import rename

Co-Authored-By: Clay Gerrard <clay.gerrard@gmail.com>
Change-Id: I1eaf075ff9855cfa03e7991bdf33375b0e4397e6
This commit is contained in:
Tim Burke 2019-11-19 21:25:45 -08:00
parent b3cd0cd4bb
commit 1f7b97ec0f
13 changed files with 280 additions and 249 deletions

View File

@ -29,11 +29,11 @@ from six.moves.http_client import HTTPException
from swift.common.bufferedhttp import http_connect from swift.common.bufferedhttp import http_connect
from swift.common.exceptions import ClientException from swift.common.exceptions import ClientException
from swift.common.utils import Timestamp, FileLikeIter from swift.common.swob import normalize_etag
from swift.common.utils import Timestamp, FileLikeIter, quote
from swift.common.http import HTTP_NO_CONTENT, HTTP_INSUFFICIENT_STORAGE, \ from swift.common.http import HTTP_NO_CONTENT, HTTP_INSUFFICIENT_STORAGE, \
is_success, is_server_error is_success, is_server_error
from swift.common.header_key_dict import HeaderKeyDict from swift.common.header_key_dict import HeaderKeyDict
from swift.common.utils import quote
class DirectClientException(ClientException): class DirectClientException(ClientException):
@ -485,7 +485,7 @@ def direct_put_object(node, part, account, container, name, contents,
if headers is None: if headers is None:
headers = {} headers = {}
if etag: if etag:
headers['ETag'] = etag.strip('"') headers['ETag'] = normalize_etag(etag)
if content_type is not None: if content_type is not None:
headers['Content-Type'] = content_type headers['Content-Type'] = content_type
else: else:
@ -498,7 +498,7 @@ def direct_put_object(node, part, account, container, name, contents,
'Object', conn_timeout, response_timeout, contents=contents, 'Object', conn_timeout, response_timeout, contents=contents,
content_length=content_length, chunk_size=chunk_size) content_length=content_length, chunk_size=chunk_size)
return resp.getheader('etag').strip('"') return normalize_etag(resp.getheader('etag'))
def direct_post_object(node, part, account, container, name, headers, def direct_post_object(node, part, account, container, name, headers,

View File

@ -25,7 +25,7 @@ from swift.common.request_helpers import get_object_transient_sysmeta, \
strip_user_meta_prefix, is_user_meta, update_etag_is_at_header, \ strip_user_meta_prefix, is_user_meta, update_etag_is_at_header, \
get_container_update_override_key get_container_update_override_key
from swift.common.swob import Request, Match, HTTPException, \ from swift.common.swob import Request, Match, HTTPException, \
HTTPUnprocessableEntity, wsgi_to_bytes, bytes_to_wsgi HTTPUnprocessableEntity, wsgi_to_bytes, bytes_to_wsgi, normalize_etag
from swift.common.utils import get_logger, config_true_value, \ from swift.common.utils import get_logger, config_true_value, \
MD5_OF_EMPTY_STRING MD5_OF_EMPTY_STRING
@ -263,7 +263,7 @@ class EncrypterObjContext(CryptoWSGIContext):
ciphertext_etag = enc_input_proxy.ciphertext_md5.hexdigest() ciphertext_etag = enc_input_proxy.ciphertext_md5.hexdigest()
mod_resp_headers = [ mod_resp_headers = [
(h, v if (h.lower() != 'etag' or (h, v if (h.lower() != 'etag' or
v.strip('"') != ciphertext_etag) normalize_etag(v) != ciphertext_etag)
else plaintext_etag) else plaintext_etag)
for h, v in mod_resp_headers] for h, v in mod_resp_headers]

View File

@ -128,7 +128,7 @@ from swift.common.exceptions import ListingIterError, SegmentError
from swift.common.http import is_success from swift.common.http import is_success
from swift.common.swob import Request, Response, \ from swift.common.swob import Request, Response, \
HTTPRequestedRangeNotSatisfiable, HTTPBadRequest, HTTPConflict, \ HTTPRequestedRangeNotSatisfiable, HTTPBadRequest, HTTPConflict, \
str_to_wsgi, wsgi_to_str, wsgi_quote, wsgi_unquote str_to_wsgi, wsgi_to_str, wsgi_quote, wsgi_unquote, normalize_etag
from swift.common.utils import get_logger, \ from swift.common.utils import get_logger, \
RateLimitedIterator, quote, close_if_possible, closing_if_possible RateLimitedIterator, quote, close_if_possible, closing_if_possible
from swift.common.request_helpers import SegmentedIterable from swift.common.request_helpers import SegmentedIterable
@ -333,7 +333,7 @@ class GetContext(WSGIContext):
if h.lower() != "etag"] if h.lower() != "etag"]
etag = md5() etag = md5()
for seg_dict in segments: for seg_dict in segments:
etag.update(seg_dict['hash'].strip('"').encode('utf8')) etag.update(normalize_etag(seg_dict['hash']).encode('utf8'))
response_headers.append(('Etag', '"%s"' % etag.hexdigest())) response_headers.append(('Etag', '"%s"' % etag.hexdigest()))
app_iter = None app_iter = None

View File

@ -232,7 +232,7 @@ class BucketController(Controller):
etag = o['s3_etag'] etag = o['s3_etag']
elif 'slo_etag' in o: elif 'slo_etag' in o:
# SLOs may be in something *close* to the MU format # SLOs may be in something *close* to the MU format
etag = '"%s-N"' % o['slo_etag'].strip('"') etag = '"%s-N"' % swob.normalize_etag(o['slo_etag'])
else: else:
etag = o['hash'] etag = o['hash']
if len(etag) < 2 or etag[::len(etag) - 1] != '""': if len(etag) < 2 or etag[::len(etag) - 1] != '""':

View File

@ -68,7 +68,7 @@ import time
import six import six
from swift.common.swob import Range, bytes_to_wsgi from swift.common.swob import Range, bytes_to_wsgi, normalize_etag
from swift.common.utils import json, public, reiterate from swift.common.utils import json, public, reiterate
from swift.common.db import utf8encode from swift.common.db import utf8encode
from swift.common.request_helpers import get_container_update_override_key from swift.common.request_helpers import get_container_update_override_key
@ -620,10 +620,7 @@ class UploadController(Controller):
raise InvalidPartOrder(upload_id=upload_id) raise InvalidPartOrder(upload_id=upload_id)
previous_number = part_number previous_number = part_number
etag = part_elem.find('./ETag').text etag = normalize_etag(part_elem.find('./ETag').text)
if len(etag) >= 2 and etag[0] == '"' and etag[-1] == '"':
# strip double quotes
etag = etag[1:-1]
if len(etag) != 32 or any(c not in '0123456789abcdef' if len(etag) != 32 or any(c not in '0123456789abcdef'
for c in etag): for c in etag):
raise InvalidPart(upload_id=upload_id, raise InvalidPart(upload_id=upload_id,

View File

@ -15,7 +15,8 @@
from swift.common.http import HTTP_OK, HTTP_PARTIAL_CONTENT, HTTP_NO_CONTENT from swift.common.http import HTTP_OK, HTTP_PARTIAL_CONTENT, HTTP_NO_CONTENT
from swift.common.request_helpers import update_etag_is_at_header from swift.common.request_helpers import update_etag_is_at_header
from swift.common.swob import Range, content_range_header_value from swift.common.swob import Range, content_range_header_value, \
normalize_etag
from swift.common.utils import public, list_from_csv from swift.common.utils import public, list_from_csv
from swift.common.middleware.s3api.utils import S3Timestamp, sysmeta_header from swift.common.middleware.s3api.utils import S3Timestamp, sysmeta_header
@ -68,8 +69,7 @@ class ObjectController(Controller):
continue continue
had_match = True had_match = True
for value in list_from_csv(req.headers[match_header]): for value in list_from_csv(req.headers[match_header]):
if value.startswith('"') and value.endswith('"'): value = normalize_etag(value)
value = value[1:-1]
if value.endswith('-N'): if value.endswith('-N'):
# Deal with fake S3-like etags for SLOs uploaded via Swift # Deal with fake S3-like etags for SLOs uploaded via Swift
req.headers[match_header] += ', ' + value[:-2] req.headers[match_header] += ', ' + value[:-2]

View File

@ -331,7 +331,7 @@ from swift.common.swob import Request, HTTPBadRequest, HTTPServerError, \
HTTPMethodNotAllowed, HTTPRequestEntityTooLarge, HTTPLengthRequired, \ HTTPMethodNotAllowed, HTTPRequestEntityTooLarge, HTTPLengthRequired, \
HTTPOk, HTTPPreconditionFailed, HTTPException, HTTPNotFound, \ HTTPOk, HTTPPreconditionFailed, HTTPException, HTTPNotFound, \
HTTPUnauthorized, HTTPConflict, HTTPUnprocessableEntity, \ HTTPUnauthorized, HTTPConflict, HTTPUnprocessableEntity, \
HTTPServiceUnavailable, Response, Range, \ HTTPServiceUnavailable, Response, Range, normalize_etag, \
RESPONSE_REASONS, str_to_wsgi, wsgi_to_str, wsgi_quote RESPONSE_REASONS, str_to_wsgi, wsgi_to_str, wsgi_quote
from swift.common.utils import get_logger, config_true_value, \ from swift.common.utils import get_logger, config_true_value, \
get_valid_utf8_str, override_bytes_from_content_type, split_path, \ get_valid_utf8_str, override_bytes_from_content_type, split_path, \
@ -1324,8 +1324,8 @@ class StaticLargeObject(object):
slo_etag.update(r.encode('ascii') if six.PY3 else r) slo_etag.update(r.encode('ascii') if six.PY3 else r)
slo_etag = slo_etag.hexdigest() slo_etag = slo_etag.hexdigest()
client_etag = req.headers.get('Etag') client_etag = normalize_etag(req.headers.get('Etag'))
if client_etag and client_etag.strip('"') != slo_etag: if client_etag and client_etag != slo_etag:
err = HTTPUnprocessableEntity(request=req) err = HTTPUnprocessableEntity(request=req)
if heartbeat: if heartbeat:
resp_dict = {} resp_dict = {}

View File

@ -689,6 +689,12 @@ class Range(object):
return all_ranges return all_ranges
def normalize_etag(tag):
if tag and tag.startswith('"') and tag.endswith('"') and tag != '"':
return tag[1:-1]
return tag
class Match(object): class Match(object):
""" """
Wraps a Request's If-[None-]Match header as a friendly object. Wraps a Request's If-[None-]Match header as a friendly object.
@ -701,15 +707,10 @@ class Match(object):
tag = tag.strip() tag = tag.strip()
if not tag: if not tag:
continue continue
if tag.startswith('"') and tag.endswith('"'): self.tags.add(normalize_etag(tag))
self.tags.add(tag[1:-1])
else:
self.tags.add(tag)
def __contains__(self, val): def __contains__(self, val):
if val and val.startswith('"') and val.endswith('"'): return '*' in self.tags or normalize_etag(val) in self.tags
val = val[1:-1]
return '*' in self.tags or val in self.tags
def __repr__(self): def __repr__(self):
return '%s(%r)' % ( return '%s(%r)' % (

View File

@ -36,6 +36,7 @@ from swift.common.internal_client import (
from swift.common.exceptions import ClientException from swift.common.exceptions import ClientException
from swift.common.ring import Ring from swift.common.ring import Ring
from swift.common.ring.utils import is_local_device from swift.common.ring.utils import is_local_device
from swift.common.swob import normalize_etag
from swift.common.utils import ( from swift.common.utils import (
clean_content_type, config_true_value, clean_content_type, config_true_value,
FileLikeIter, get_logger, hash_path, quote, validate_sync_to, FileLikeIter, get_logger, hash_path, quote, validate_sync_to,
@ -607,7 +608,7 @@ class ContainerSync(Daemon):
if key in headers: if key in headers:
del headers[key] del headers[key]
if 'etag' in headers: if 'etag' in headers:
headers['etag'] = headers['etag'].strip('"') headers['etag'] = normalize_etag(headers['etag'])
if 'content-type' in headers: if 'content-type' in headers:
headers['content-type'] = clean_content_type( headers['content-type'] = clean_content_type(
headers['content-type']) headers['content-type'])

View File

@ -58,7 +58,7 @@ from swift.common.swob import HTTPAccepted, HTTPBadRequest, HTTPCreated, \
HTTPPreconditionFailed, HTTPRequestTimeout, HTTPUnprocessableEntity, \ HTTPPreconditionFailed, HTTPRequestTimeout, HTTPUnprocessableEntity, \
HTTPClientDisconnect, HTTPMethodNotAllowed, Request, Response, \ HTTPClientDisconnect, HTTPMethodNotAllowed, Request, Response, \
HTTPInsufficientStorage, HTTPForbidden, HTTPException, HTTPConflict, \ HTTPInsufficientStorage, HTTPForbidden, HTTPException, HTTPConflict, \
HTTPServerError, wsgi_to_bytes, wsgi_to_str HTTPServerError, wsgi_to_bytes, wsgi_to_str, normalize_etag
from swift.obj.diskfile import RESERVED_DATAFILE_META, DiskFileRouter from swift.obj.diskfile import RESERVED_DATAFILE_META, DiskFileRouter
from swift.obj.expirer import build_task_obj from swift.obj.expirer import build_task_obj
@ -942,8 +942,8 @@ class ObjectController(BaseStorageServer):
if (is_sys_or_user_meta('object', val[0]) or if (is_sys_or_user_meta('object', val[0]) or
is_object_transient_sysmeta(val[0]))) is_object_transient_sysmeta(val[0])))
# N.B. footers_metadata is a HeaderKeyDict # N.B. footers_metadata is a HeaderKeyDict
received_etag = footers_metadata.get('etag', request.headers.get( received_etag = normalize_etag(footers_metadata.get(
'etag', '')).strip('"') 'etag', request.headers.get('etag', '')))
if received_etag and received_etag != metadata['ETag']: if received_etag and received_etag != metadata['ETag']:
raise HTTPUnprocessableEntity(request=request) raise HTTPUnprocessableEntity(request=request)

View File

@ -57,7 +57,7 @@ from swift.common.http import is_informational, is_success, is_redirection, \
HTTP_INSUFFICIENT_STORAGE, HTTP_UNAUTHORIZED, HTTP_CONTINUE, HTTP_GONE HTTP_INSUFFICIENT_STORAGE, HTTP_UNAUTHORIZED, HTTP_CONTINUE, HTTP_GONE
from swift.common.swob import Request, Response, Range, \ from swift.common.swob import Request, Response, Range, \
HTTPException, HTTPRequestedRangeNotSatisfiable, HTTPServiceUnavailable, \ HTTPException, HTTPRequestedRangeNotSatisfiable, HTTPServiceUnavailable, \
status_map, wsgi_to_str, str_to_wsgi, wsgi_quote status_map, wsgi_to_str, str_to_wsgi, wsgi_quote, normalize_etag
from swift.common.request_helpers import strip_sys_meta_prefix, \ from swift.common.request_helpers import strip_sys_meta_prefix, \
strip_user_meta_prefix, is_user_meta, is_sys_meta, is_sys_or_user_meta, \ strip_user_meta_prefix, is_user_meta, is_sys_meta, is_sys_or_user_meta, \
http_response_to_document_iters, is_object_transient_sysmeta, \ http_response_to_document_iters, is_object_transient_sysmeta, \
@ -1268,9 +1268,9 @@ class ResumingGetter(object):
close_swift_conn(possible_source) close_swift_conn(possible_source)
else: else:
if self.used_source_etag and \ if self.used_source_etag and \
self.used_source_etag != src_headers.get( self.used_source_etag != normalize_etag(src_headers.get(
'x-object-sysmeta-ec-etag', 'x-object-sysmeta-ec-etag',
src_headers.get('etag', '')).strip('"'): src_headers.get('etag', ''))):
self.statuses.append(HTTP_NOT_FOUND) self.statuses.append(HTTP_NOT_FOUND)
self.reasons.append('') self.reasons.append('')
self.bodies.append('') self.bodies.append('')
@ -1373,9 +1373,8 @@ class ResumingGetter(object):
# from the same object (EC). Otherwise, if the cluster has two # from the same object (EC). Otherwise, if the cluster has two
# versions of the same object, we might end up switching between # versions of the same object, we might end up switching between
# old and new mid-stream and giving garbage to the client. # old and new mid-stream and giving garbage to the client.
self.used_source_etag = src_headers.get( self.used_source_etag = normalize_etag(src_headers.get(
'x-object-sysmeta-ec-etag', 'x-object-sysmeta-ec-etag', src_headers.get('etag', '')))
src_headers.get('etag', '')).strip('"')
self.node = node self.node = node
return source, node return source, node
return None, None return None, None
@ -1922,7 +1921,7 @@ class Controller(object):
if headers: if headers:
update_headers(resp, headers[status_index]) update_headers(resp, headers[status_index])
if etag: if etag:
resp.headers['etag'] = etag.strip('"') resp.headers['etag'] = normalize_etag(etag)
return resp return resp
return None return None

View File

@ -70,7 +70,8 @@ from swift.common.swob import HTTPAccepted, HTTPBadRequest, HTTPNotFound, \
HTTPPreconditionFailed, HTTPRequestEntityTooLarge, HTTPRequestTimeout, \ HTTPPreconditionFailed, HTTPRequestEntityTooLarge, HTTPRequestTimeout, \
HTTPServerError, HTTPServiceUnavailable, HTTPClientDisconnect, \ HTTPServerError, HTTPServiceUnavailable, HTTPClientDisconnect, \
HTTPUnprocessableEntity, Response, HTTPException, \ HTTPUnprocessableEntity, Response, HTTPException, \
HTTPRequestedRangeNotSatisfiable, Range, HTTPInternalServerError HTTPRequestedRangeNotSatisfiable, Range, HTTPInternalServerError, \
normalize_etag
from swift.common.request_helpers import update_etag_is_at_header, \ from swift.common.request_helpers import update_etag_is_at_header, \
resolve_etag_is_at_header, validate_internal_obj resolve_etag_is_at_header, validate_internal_obj
@ -478,7 +479,7 @@ class BaseObjectController(Controller):
{'status': response.status, {'status': response.status,
'body': body[:1024], 'path': req.path}) 'body': body[:1024], 'path': req.path})
elif is_success(response.status): elif is_success(response.status):
etags.add(response.getheader('etag').strip('"')) etags.add(normalize_etag(response.getheader('etag')))
for (putter, response) in pile: for (putter, response) in pile:
if response: if response:
@ -2638,8 +2639,8 @@ class ECObjectController(BaseObjectController):
computed_etag = (etag_hasher.hexdigest() computed_etag = (etag_hasher.hexdigest()
if etag_hasher else None) if etag_hasher else None)
footers = self._get_footers(req) footers = self._get_footers(req)
received_etag = footers.get('etag', req.headers.get( received_etag = normalize_etag(footers.get(
'etag', '')).strip('"') 'etag', req.headers.get('etag', '')))
if (computed_etag and received_etag and if (computed_etag and received_etag and
computed_etag != received_etag): computed_etag != received_etag):
raise HTTPUnprocessableEntity(request=req) raise HTTPUnprocessableEntity(request=req)

File diff suppressed because it is too large Load Diff