-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Request/Response Checksum Behavior Updates #3277
Changes from 21 commits
cdcb6b0
c38b116
02fac6d
50d7142
0a2971f
bf16ed7
88eb03b
878cb93
7563334
860dc3b
2e24f7f
e9eade8
ce46e4a
d44bb57
dc710ec
0569615
626c08a
8f26df1
253928d
ff4f122
a2b19bd
503ead6
509440a
43ba7d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,17 +25,15 @@ | |
from binascii import crc32 | ||
from hashlib import sha1, sha256 | ||
|
||
from botocore.compat import HAS_CRT | ||
from botocore.compat import HAS_CRT, urlparse | ||
from botocore.exceptions import ( | ||
AwsChunkedWrapperError, | ||
FlexibleChecksumError, | ||
MissingDependencyException, | ||
) | ||
from botocore.model import StructureShape | ||
from botocore.response import StreamingBody | ||
from botocore.utils import ( | ||
conditionally_calculate_md5, | ||
determine_content_length, | ||
) | ||
from botocore.utils import determine_content_length, has_checksum_header | ||
|
||
if HAS_CRT: | ||
from awscrt import checksums as crt_checksums | ||
|
@@ -44,6 +42,8 @@ | |
|
||
logger = logging.getLogger(__name__) | ||
|
||
DEFAULT_CHECKSUM_ALGORITHM = "CRC32" | ||
|
||
|
||
class BaseChecksum: | ||
_CHUNK_SIZE = 1024 * 1024 | ||
|
@@ -259,7 +259,19 @@ def resolve_request_checksum_algorithm( | |
params, | ||
supported_algorithms=None, | ||
): | ||
# If the header is already set by the customer, skip calculation | ||
if has_checksum_header(request): | ||
return | ||
|
||
extra_headers = {} | ||
request_checksum_calculation = request["context"][ | ||
SamRemis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"client_config" | ||
].request_checksum_calculation | ||
http_checksum = operation_model.http_checksum | ||
request_checksum_required = ( | ||
operation_model.http_checksum_required | ||
SamRemis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
or http_checksum.get("requestChecksumRequired") | ||
) | ||
algorithm_member = http_checksum.get("requestAlgorithmMember") | ||
if algorithm_member and algorithm_member in params: | ||
# If the client has opted into using flexible checksums and the | ||
|
@@ -280,35 +292,59 @@ def resolve_request_checksum_algorithm( | |
raise FlexibleChecksumError( | ||
error_msg=f"Unsupported checksum algorithm: {algorithm_name}" | ||
) | ||
elif request_checksum_required or ( | ||
algorithm_member and request_checksum_calculation == "when_supported" | ||
): | ||
algorithm_name = DEFAULT_CHECKSUM_ALGORITHM.lower() | ||
algorithm_member_header = _get_request_algorithm_member_header( | ||
operation_model, request, algorithm_member | ||
) | ||
if algorithm_member_header is not None: | ||
extra_headers[algorithm_member_header] = DEFAULT_CHECKSUM_ALGORITHM | ||
else: | ||
return | ||
|
||
location_type = "header" | ||
if operation_model.has_streaming_input: | ||
# Operations with streaming input must support trailers. | ||
if request["url"].startswith("https:"): | ||
# We only support unsigned trailer checksums currently. As this | ||
# disables payload signing we'll only use trailers over TLS. | ||
location_type = "trailer" | ||
|
||
algorithm = { | ||
"algorithm": algorithm_name, | ||
"in": location_type, | ||
"name": f"x-amz-checksum-{algorithm_name}", | ||
} | ||
location_type = "header" | ||
if ( | ||
operation_model.has_streaming_input | ||
and urlparse(request["url"]).scheme == "https" | ||
): | ||
# Operations with streaming input must support trailers. | ||
# We only support unsigned trailer checksums currently. As this | ||
# disables payload signing we'll only use trailers over TLS. | ||
location_type = "trailer" | ||
pass | ||
jonathan343 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
algorithm = { | ||
"algorithm": algorithm_name, | ||
"in": location_type, | ||
"name": f"x-amz-checksum-{algorithm_name}", | ||
} | ||
if extra_headers: | ||
algorithm["extra_headers"] = extra_headers | ||
|
||
if algorithm["name"] in request["headers"]: | ||
# If the header is already set by the customer, skip calculation | ||
return | ||
checksum_context = request["context"].get("checksum", {}) | ||
checksum_context["request_algorithm"] = algorithm | ||
request["context"]["checksum"] = checksum_context | ||
|
||
checksum_context = request["context"].get("checksum", {}) | ||
checksum_context["request_algorithm"] = algorithm | ||
request["context"]["checksum"] = checksum_context | ||
elif operation_model.http_checksum_required or http_checksum.get( | ||
"requestChecksumRequired" | ||
): | ||
# Otherwise apply the old http checksum behavior via Content-MD5 | ||
checksum_context = request["context"].get("checksum", {}) | ||
checksum_context["request_algorithm"] = "conditional-md5" | ||
request["context"]["checksum"] = checksum_context | ||
|
||
def _get_request_algorithm_member_header( | ||
operation_model, request, algorithm_member | ||
): | ||
"""Get the name of the header targeted by the "requestAlgorithmMember".""" | ||
operation_input_shape = operation_model.input_shape | ||
if not isinstance(operation_input_shape, StructureShape): | ||
return | ||
|
||
algorithm_member_shape = operation_input_shape.members.get( | ||
algorithm_member | ||
) | ||
|
||
return ( | ||
algorithm_member_shape.serialization.get("name") | ||
if algorithm_member_shape | ||
else None | ||
) | ||
|
||
|
||
def apply_request_checksum(request): | ||
|
@@ -318,17 +354,16 @@ def apply_request_checksum(request): | |
if not algorithm: | ||
return | ||
|
||
if algorithm == "conditional-md5": | ||
# Special case to handle the http checksum required trait | ||
conditionally_calculate_md5(request) | ||
elif algorithm["in"] == "header": | ||
if algorithm["in"] == "header": | ||
_apply_request_header_checksum(request) | ||
elif algorithm["in"] == "trailer": | ||
_apply_request_trailer_checksum(request) | ||
else: | ||
raise FlexibleChecksumError( | ||
error_msg="Unknown checksum variant: {}".format(algorithm["in"]) | ||
) | ||
if "extra_headers" in algorithm: | ||
request["headers"].update(algorithm["extra_headers"]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does this work if the user already supplied these headers? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The header we're worried about would only get added when the default checksum is used. This means that the user didn't supply the |
||
|
||
|
||
def _apply_request_header_checksum(request): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have a rough idea of what expansion we're expecting here? Generally grab-bag style containers like this tend to grow in unexpected ways. It may be worth constraining to the exact header we're expecting since it's tightly coupled to the checksum we're creating.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated this to instead store the following in our request checksum context:
We can later check
if "request_algorithm_header" in request["context"]["checksum"]
and apply the header if it exists. Let me know if that's better.