-
Notifications
You must be signed in to change notification settings - Fork 658
fix(boto3): Fix botocore SigV4 failures caused by post-sign trace propagation #7050
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
base: master
Are you sure you want to change the base?
Changes from all commits
abc57bc
78f1ee6
88fdd7d
a1e5f9c
4ba8901
92672ca
37fa9ff
e675673
14bad29
90a77d6
f2d0cee
e6e9e5e
3f4dacc
6d7301c
a0c1ad2
e40e931
af905db
09f8c44
e07978e
08dc4d2
079cf17
ebcce02
484a941
35ff7b4
355be07
5ab6ccb
f04bd51
28e33cc
6ff70c4
8003db0
b68e9f7
3ce7587
628543e
a739e84
3d44029
c706b7b
1ee3b58
8762b88
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 | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,7 +10,7 @@ | |||||||||
| from sentry_sdk.integrations import Integration | ||||||||||
| from sentry_sdk.scope import add_global_event_processor, should_send_default_pii | ||||||||||
| from sentry_sdk.traces import StreamedSpan | ||||||||||
| from sentry_sdk.tracing import Span | ||||||||||
| from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME, Span | ||||||||||
| from sentry_sdk.tracing_utils import ( | ||||||||||
| EnvironHeaders, | ||||||||||
| add_http_breadcrumb, | ||||||||||
|
|
@@ -20,6 +20,8 @@ | |||||||||
| ) | ||||||||||
| from sentry_sdk.utils import ( | ||||||||||
| SENSITIVE_DATA_SUBSTITUTE, | ||||||||||
| _get_aws_sigv4_signed_headers_from_authorization_header, | ||||||||||
| _get_aws_sigv4_signed_headers_from_url_query_string, | ||||||||||
| capture_internal_exceptions, | ||||||||||
| ensure_integration_enabled, | ||||||||||
| is_sentry_url, | ||||||||||
|
|
@@ -29,7 +31,7 @@ | |||||||||
| ) | ||||||||||
|
|
||||||||||
| if TYPE_CHECKING: | ||||||||||
| from typing import Any, Callable, Dict, List, Optional, Union | ||||||||||
| from typing import Any, Callable, Dict, List, Optional, Tuple, Union | ||||||||||
|
|
||||||||||
| from sentry_sdk._types import Event, Hint | ||||||||||
|
|
||||||||||
|
|
@@ -40,6 +42,14 @@ | |||||||||
| "build": sys.version, | ||||||||||
| } | ||||||||||
|
|
||||||||||
| _SENTRY_HEADER_NAMES = frozenset((BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME)) | ||||||||||
|
|
||||||||||
| try: | ||||||||||
| from botocore.awsrequest import AWSHTTPConnection, AWSHTTPSConnection | ||||||||||
| except ImportError: | ||||||||||
| AWSHTTPConnection = None # type: ignore[misc,assignment] | ||||||||||
| AWSHTTPSConnection = None # type: ignore[misc,assignment] | ||||||||||
|
Check failure on line 51 in sentry_sdk/integrations/stdlib.py
|
||||||||||
|
|
||||||||||
|
|
||||||||||
| class StdlibIntegration(Integration): | ||||||||||
| identifier = "stdlib" | ||||||||||
|
Check failure on line 55 in sentry_sdk/integrations/stdlib.py
|
||||||||||
|
|
@@ -73,7 +83,178 @@ | |||||||||
| add_http_request_source(span) | ||||||||||
|
|
||||||||||
|
|
||||||||||
| def _get_wrapped_putheader( | ||||||||||
| original_putheader: "Callable[..., Any]", | ||||||||||
| ) -> "Callable[..., Any]": | ||||||||||
| """ | ||||||||||
| Responsible for tracking which sentry headers are present and whether | ||||||||||
| they are listed in AWS SigV4 `SignedHeaders`. | ||||||||||
| """ | ||||||||||
|
|
||||||||||
| def putheader(self: "HTTPConnection", header: "Any", *values: "Any") -> "Any": | ||||||||||
| rv = original_putheader(self, header, *values) | ||||||||||
|
|
||||||||||
| request_headers: "Optional[Dict[str, Tuple[bool, bool]]]" = getattr( | ||||||||||
| self, "_sentrysdk_request_headers", None | ||||||||||
| ) | ||||||||||
| if request_headers is None: | ||||||||||
| return rv | ||||||||||
|
|
||||||||||
| if isinstance(header, bytes): | ||||||||||
| normalized_header = header.decode("ascii", "ignore").lower() | ||||||||||
| elif isinstance(header, str): | ||||||||||
| normalized_header = header.lower() | ||||||||||
| else: | ||||||||||
| return rv | ||||||||||
|
|
||||||||||
| if normalized_header in _SENTRY_HEADER_NAMES: | ||||||||||
| _, is_signed = request_headers.get(normalized_header, (False, False)) | ||||||||||
| request_headers[normalized_header] = (True, is_signed) | ||||||||||
|
|
||||||||||
| if normalized_header == "authorization" and values: | ||||||||||
| with capture_internal_exceptions(): | ||||||||||
| authorization = values[0] | ||||||||||
| if isinstance(authorization, bytes): | ||||||||||
| authorization = authorization.decode("latin-1") | ||||||||||
| for ( | ||||||||||
| signed_header | ||||||||||
| ) in _get_aws_sigv4_signed_headers_from_authorization_header( | ||||||||||
| authorization | ||||||||||
| ): | ||||||||||
| if signed_header in _SENTRY_HEADER_NAMES: | ||||||||||
| is_present, _ = request_headers.get( | ||||||||||
| signed_header, (False, False) | ||||||||||
| ) | ||||||||||
| request_headers[signed_header] = (is_present, True) | ||||||||||
|
|
||||||||||
| return rv | ||||||||||
|
|
||||||||||
| return putheader | ||||||||||
|
|
||||||||||
|
|
||||||||||
| def _get_wrapped_endheaders( | ||||||||||
| original_endheaders: "Callable[..., Any]", | ||||||||||
| ) -> "Callable[..., Any]": | ||||||||||
| """ | ||||||||||
| Responsible for injecting trace propagation headers, ensuring that the request is not invalidated | ||||||||||
| by honoring signed headers. | ||||||||||
| """ | ||||||||||
|
|
||||||||||
| def endheaders(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any": | ||||||||||
| real_url = getattr(self, "_sentrysdk_trace_url", None) | ||||||||||
|
Member
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. Does
Member
Author
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. yes, it represents the whole URL, but its mainly used for trace propagation. |
||||||||||
| span = getattr(self, "_sentrysdk_span", None) | ||||||||||
|
|
||||||||||
| try: | ||||||||||
| if real_url is not None: | ||||||||||
| with capture_internal_exceptions(): | ||||||||||
| request_headers: "Optional[Dict[str, Tuple[bool, bool]]]" = getattr( | ||||||||||
| self, "_sentrysdk_request_headers", {} | ||||||||||
| ) | ||||||||||
| if request_headers is not None: | ||||||||||
|
Comment on lines
+151
to
+153
Member
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. If If this is the intent, it'd be clearer to have the default value for |
||||||||||
| for ( | ||||||||||
| signed_header | ||||||||||
| ) in _get_aws_sigv4_signed_headers_from_url_query_string( | ||||||||||
| real_url | ||||||||||
| ): | ||||||||||
| if signed_header in _SENTRY_HEADER_NAMES: | ||||||||||
| is_present, _ = request_headers.get( | ||||||||||
| signed_header, (False, False) | ||||||||||
| ) | ||||||||||
| request_headers[signed_header] = (is_present, True) | ||||||||||
|
Comment on lines
+154
to
+163
Member
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. I'm finding this block of code (and the related one further up that deals with the authorization header) a bit confusing. In what circumstances would the Related to that - do we need the |
||||||||||
|
|
||||||||||
| for ( | ||||||||||
| header_name, | ||||||||||
| header_value, | ||||||||||
| ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( | ||||||||||
| span=span | ||||||||||
| ): | ||||||||||
| normalized_header = header_name.lower() | ||||||||||
| # preserve signed headers and avoid duplicate `sentry-trace`. | ||||||||||
| is_present, is_signed = request_headers.get( | ||||||||||
| normalized_header, (False, False) | ||||||||||
| ) | ||||||||||
| if is_signed or ( | ||||||||||
| is_present and normalized_header != BAGGAGE_HEADER_NAME | ||||||||||
|
Member
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. If we're looking to avoid a duplicate |
||||||||||
| ): | ||||||||||
| continue | ||||||||||
|
|
||||||||||
| logger.debug( | ||||||||||
| "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( | ||||||||||
| key=header_name, | ||||||||||
| value=header_value, | ||||||||||
| real_url=real_url, | ||||||||||
| ) | ||||||||||
| ) | ||||||||||
| self.putheader(header_name, header_value) | ||||||||||
| return original_endheaders(self, *args, **kwargs) | ||||||||||
| finally: | ||||||||||
| self._sentrysdk_trace_url = None # type: ignore[attr-defined] | ||||||||||
|
|
||||||||||
| return endheaders | ||||||||||
|
|
||||||||||
|
|
||||||||||
| def _get_wrapped_putrequest( | ||||||||||
| original_putrequest: "Callable[..., Any]", | ||||||||||
| ) -> "Callable[..., Any]": | ||||||||||
| """ | ||||||||||
| Responsible for initializing request and signed header tracking on the instance. | ||||||||||
| """ | ||||||||||
|
|
||||||||||
| def putrequest( | ||||||||||
| self: "HTTPConnection", method: str, url: str, *args: "Any", **kwargs: "Any" | ||||||||||
| ) -> "Any": | ||||||||||
| # track which propagation headers are present and signed e.g. {"sentry-trace": (is_present, is_signed)} | ||||||||||
| request_headers: "Optional[Dict[str, Tuple[bool, bool]]]" = {} | ||||||||||
| self._sentrysdk_request_headers = request_headers # type: ignore[attr-defined] | ||||||||||
|
Comment on lines
+206
to
+208
Member
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. Was there code that was previously here that used
Suggested change
Member
Author
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. I was trying to fix a mypy error, but just did |
||||||||||
|
|
||||||||||
| try: | ||||||||||
| rv = original_putrequest(self, method, url, *args, **kwargs) | ||||||||||
| except BaseException: | ||||||||||
| self._sentrysdk_request_headers = None # type: ignore[attr-defined] | ||||||||||
| raise | ||||||||||
|
|
||||||||||
| return rv | ||||||||||
|
|
||||||||||
| return putrequest | ||||||||||
|
|
||||||||||
|
|
||||||||||
| def _patch_aws_connection() -> None: | ||||||||||
| """ | ||||||||||
| Patch AWS connection classes. These classes provide functions to sign HTTP headers, and subsequently | ||||||||||
| injecting trace propagation headers would invalidate the request. | ||||||||||
|
|
||||||||||
| Detect whether propagation headers are present and signed by patching | ||||||||||
| `putheader()`. Store that state in `_sentrysdk_request_headers`, initialized | ||||||||||
| by the `putrequest()` patch. | ||||||||||
|
|
||||||||||
| Do not edit signed headers when adding trace propagation headers in the `endheaders()` patch. | ||||||||||
| """ | ||||||||||
| if AWSHTTPConnection is not None: | ||||||||||
| AWSHTTPConnection.putheader = _get_wrapped_putheader( # type: ignore[method-assign] | ||||||||||
| AWSHTTPConnection.putheader | ||||||||||
| ) | ||||||||||
| AWSHTTPConnection.endheaders = _get_wrapped_endheaders( # type: ignore[method-assign] | ||||||||||
| AWSHTTPConnection.endheaders | ||||||||||
| ) | ||||||||||
| AWSHTTPConnection.putrequest = _get_wrapped_putrequest( # type: ignore[method-assign] | ||||||||||
| AWSHTTPConnection.putrequest | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| if AWSHTTPSConnection is not None: | ||||||||||
| AWSHTTPSConnection.putheader = _get_wrapped_putheader( # type: ignore[method-assign] | ||||||||||
| AWSHTTPSConnection.putheader | ||||||||||
| ) | ||||||||||
| AWSHTTPSConnection.endheaders = _get_wrapped_endheaders( # type: ignore[method-assign] | ||||||||||
| AWSHTTPSConnection.endheaders | ||||||||||
| ) | ||||||||||
| AWSHTTPSConnection.putrequest = _get_wrapped_putrequest( # type: ignore[method-assign] | ||||||||||
| AWSHTTPSConnection.putrequest | ||||||||||
|
Check failure on line 251 in sentry_sdk/integrations/stdlib.py
|
||||||||||
| ) | ||||||||||
|
|
||||||||||
|
|
||||||||||
| def _install_httplib() -> None: | ||||||||||
|
Contributor
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. AWS connection wrapper bypasses stdlib putrequest instrumentation
Evidence
Identified by Warden · code-review, find-bugs · 8UW-Y37
Contributor
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. no they call the superclass
Member
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. From looking at the botocore code, I think this is legit. Here's a sample script of what it's describing. If you reverse the order of the Edit: the solution would be to move the |
||||||||||
| _patch_aws_connection() | ||||||||||
|
alexander-alderman-webb marked this conversation as resolved.
|
||||||||||
|
|
||||||||||
| real_putrequest = HTTPConnection.putrequest | ||||||||||
| real_getresponse = HTTPConnection.getresponse | ||||||||||
| real_read = HTTPResponse.read | ||||||||||
|
|
@@ -189,19 +370,25 @@ | |||||||||
|
|
||||||||||
| rv = real_putrequest(self, method, url, *args, **kwargs) | ||||||||||
|
|
||||||||||
| if should_propagate_trace(client, real_url): | ||||||||||
| # If _sentrysdk_request_headers is present, trace propagation headers should | ||||||||||
| # be injected in an `endheaders()` patch. | ||||||||||
| if should_propagate_trace(client, real_url) and not hasattr( | ||||||||||
| self, "_sentrysdk_request_headers" | ||||||||||
| ): | ||||||||||
| for ( | ||||||||||
| key, | ||||||||||
| value, | ||||||||||
| ) in sentry_sdk.get_current_scope().iter_trace_propagation_headers( | ||||||||||
| span=span | ||||||||||
| ): | ||||||||||
| logger.debug( | ||||||||||
| "[Tracing] Adding `{key}` header {value} to outgoing request to {real_url}.".format( | ||||||||||
| key=key, value=value, real_url=real_url | ||||||||||
| ) | ||||||||||
| ) | ||||||||||
| self.putheader(key, value) | ||||||||||
| elif should_propagate_trace(client, real_url): | ||||||||||
| self._sentrysdk_trace_url = real_url # type: ignore[attr-defined] | ||||||||||
|
Check failure on line 391 in sentry_sdk/integrations/stdlib.py
|
||||||||||
|
cursor[bot] marked this conversation as resolved.
|
||||||||||
|
|
||||||||||
| self._sentrysdk_span = span # type: ignore[attr-defined] | ||||||||||
| self._sentrysdk_breadcrumb = breadcrumb # type: ignore[attr-defined] | ||||||||||
|
|
||||||||||
Uh oh!
There was an error while loading. Please reload this page.