Merge pull request #3042 from derekpierre/weird-python-chars

Fix weird python chars in exception messages/logging/REST responses
pull/3043/head
KPrasch 2022-12-23 09:39:42 -08:00 committed by GitHub
commit 9ed0186060
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 171 additions and 23 deletions

View File

@ -0,0 +1 @@
Use decoded text from failed HTTP Responses for exception messages.

View File

@ -1,20 +1,15 @@
import socket
import ssl
import time
from http import HTTPStatus
from pathlib import Path
from typing import Optional, Tuple
from typing import Sequence
from typing import Optional, Sequence
import requests
from constant_sorrow.constants import EXEMPT_FROM_VERIFICATION
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.x509 import Certificate
from nucypher_core import MetadataRequest, FleetStateChecksum, NodeMetadata
from nucypher_core import FleetStateChecksum, MetadataRequest, NodeMetadata
from requests.exceptions import SSLError
from nucypher.blockchain.eth.registry import BaseContractRegistry
@ -146,22 +141,24 @@ class NucypherMiddlewareClient:
if cleaned_response.status_code >= 300:
if cleaned_response.status_code == HTTPStatus.BAD_REQUEST:
raise RestMiddleware.BadRequest(reason=cleaned_response.content)
raise RestMiddleware.BadRequest(reason=cleaned_response.text)
elif cleaned_response.status_code == HTTPStatus.NOT_FOUND:
m = f"While trying to {method_name} {args} ({kwargs}), server 404'd. Response: {cleaned_response.content}"
m = f"While trying to {method_name} {args} ({kwargs}), server 404'd. Response: {cleaned_response.text}"
raise RestMiddleware.NotFound(m)
elif cleaned_response.status_code == HTTPStatus.PAYMENT_REQUIRED:
# TODO: Use this as a hook to prompt Bob's payment for policy sponsorship
# https://getyarn.io/yarn-clip/ce0d37ba-4984-4210-9a40-c9c9859a3164
raise RestMiddleware.PaymentRequired(cleaned_response.content)
raise RestMiddleware.PaymentRequired(cleaned_response.text)
elif cleaned_response.status_code == HTTPStatus.FORBIDDEN:
raise RestMiddleware.Unauthorized(cleaned_response.content)
raise RestMiddleware.Unauthorized(cleaned_response.text)
else:
raise RestMiddleware.UnexpectedResponse(cleaned_response.content, status=cleaned_response.status_code)
raise RestMiddleware.UnexpectedResponse(
cleaned_response.text, status=cleaned_response.status_code
)
return cleaned_response
@ -218,15 +215,18 @@ class RestMiddleware:
super().__init__(message, *args, **kwargs)
class UnexpectedResponse(Exception):
"""Based for all HTTP status codes"""
def __init__(self, message, status, *args, **kwargs):
super().__init__(message, *args, **kwargs)
self.status = status
class NotFound(UnexpectedResponse):
"""Raised for HTTP 404"""
def __init__(self, *args, **kwargs):
super().__init__(status=HTTPStatus.NOT_FOUND, *args, **kwargs)
class BadRequest(UnexpectedResponse):
"""Raised for HTTP 400"""
def __init__(self, reason, *args, **kwargs):
self.reason = reason
super().__init__(message=reason, status=HTTPStatus.BAD_REQUEST, *args, **kwargs)

View File

@ -809,7 +809,11 @@ class Learner:
self.cycle_teacher_node()
if response.status_code != 200:
self.log.info("Bad response from teacher {}: {} - {}".format(current_teacher, response, response.content))
self.log.info(
"Bad response from teacher {}: {} - {}".format(
current_teacher, response, response.text
)
)
return
# TODO: we really should be checking this *before* we ask it for a node list,
@ -822,10 +826,13 @@ class Learner:
#
# Deserialize
#
response_data = response.content
try:
metadata = MetadataResponse.from_bytes(response.content)
metadata = MetadataResponse.from_bytes(response_data)
except Exception as e:
self.log.warn(f"Failed to deserialize MetadataResponse from Teacher {current_teacher} ({e}): {response.content}")
self.log.warn(
f"Failed to deserialize MetadataResponse from Teacher {current_teacher} ({e}): hex bytes={response_data.hex()}"
)
return
try:
@ -833,7 +840,8 @@ class Learner:
except Exception as e:
# TODO (#567): bucket the node as suspicious
self.log.warn(
f"Failed to verify MetadataResponse from Teacher {current_teacher} ({e}): {response.content}")
f"Failed to verify MetadataResponse from Teacher {current_teacher} ({e}): hex bytes={response_data.hex()}"
)
return
# End edge case handling.

View File

@ -163,7 +163,7 @@ def evaluate_condition_lingo(
)
except NoConnectionToChain as e:
error = EvalError(
f"Node does not have a connection to chain ID {e.chain}: {e}",
f"Node does not have a connection to chain ID {e.chain}",
HTTPStatus.NOT_IMPLEMENTED,
)
except ConditionEvaluationFailed as e:

View File

@ -4,6 +4,7 @@ import pytest
from nucypher_core import Conditions
from nucypher.characters.lawful import Ursula
from nucypher.policy.conditions.exceptions import *
from nucypher.policy.conditions.lingo import ConditionLingo
from tests.utils.middleware import MockRestMiddleware
@ -24,9 +25,12 @@ def test_single_retrieve_with_truthy_conditions(enacted_policy, bob, ursulas, mo
bob.start_learning_loop()
conditions = [
{'returnValueTest': {'value': '0', 'comparator': '>'}, 'method': 'timelock'},
{'operator': 'and'},
{'returnValueTest': {'value': '99999999999999999', 'comparator': '<'}, 'method': 'timelock'},
{"returnValueTest": {"value": 0, "comparator": ">"}, "method": "timelock"},
{"operator": "and"},
{
"returnValueTest": {"value": 99999999999999999, "comparator": "<"},
"method": "timelock",
},
]
json_conditions = json.dumps(conditions)
rust_conditions = Conditions(json_conditions)
@ -49,9 +53,11 @@ def test_single_retrieve_with_falsy_conditions(enacted_policy, bob, ursulas, moc
reencrypt_http_spy = mocker.spy(MockRestMiddleware, 'reencrypt')
# not actually used for eval, but satisfies serializers
conditions = Conditions(json.dumps(
[{'returnValueTest': {'value': '0', 'comparator': '>'}, 'method': 'timelock'}]
))
conditions = Conditions(
json.dumps(
[{"returnValueTest": {"value": 0, "comparator": ">"}, "method": "timelock"}]
)
)
bob.start_learning_loop()
@ -65,3 +71,80 @@ def test_single_retrieve_with_falsy_conditions(enacted_policy, bob, ursulas, moc
reencrypt_spy.assert_not_called()
assert isinstance(reencrypt_http_spy.spy_exception, MockRestMiddleware.Unauthorized)
FAILURE_MESSAGE = "Ive failed over and over and over again in my life. And that is why I succeed." # -- Michael Jordan
FAILURE_CASE_EXCEPTION_CODE_MATCHING = [
# (condition exception class, exception parameters, middleware exception class)
(ReturnValueEvaluationError, MockRestMiddleware.BadRequest),
(InvalidConditionLingo, MockRestMiddleware.BadRequest),
(InvalidCondition, MockRestMiddleware.BadRequest),
(RequiredContextVariable, MockRestMiddleware.BadRequest),
(InvalidContextVariableData, MockRestMiddleware.BadRequest),
(ContextVariableVerificationFailed, MockRestMiddleware.Unauthorized),
(NoConnectionToChain, MockRestMiddleware.UnexpectedResponse),
(ConditionEvaluationFailed, MockRestMiddleware.BadRequest),
(ValueError, MockRestMiddleware.UnexpectedResponse),
]
@pytest.mark.parametrize(
"eval_failure_exception_class, middleware_exception_class",
FAILURE_CASE_EXCEPTION_CODE_MATCHING,
)
def test_middleware_handling_of_failed_condition_responses(
eval_failure_exception_class,
middleware_exception_class,
mocker,
enacted_policy,
bob,
mock_rest_middleware,
):
# we use a failed condition for reencryption to test conversion of response codes to middleware exceptions
from nucypher_core import MessageKit
reencrypt_http_spy = mocker.spy(MockRestMiddleware, "reencrypt")
# not actually used for eval, but satisfies serializers
conditions = Conditions(
json.dumps(
[
{
"returnValueTest": {"value": 0, "comparator": ">"},
"method": "timelock",
}
]
)
)
bob.start_learning_loop()
message_kits = [MessageKit(enacted_policy.public_key, b"radio", conditions)]
# use string message or chain id as exception parameter
chain_id = 1
exception_parameter = (
FAILURE_MESSAGE
if eval_failure_exception_class != NoConnectionToChain
else chain_id
)
mocker.patch.object(
ConditionLingo,
"eval",
side_effect=eval_failure_exception_class(exception_parameter),
)
with pytest.raises(Ursula.NotEnoughUrsulas):
# failed retrieval because of failed exception
bob.retrieve_and_decrypt(
message_kits=message_kits,
encrypted_treasure_map=enacted_policy.treasure_map,
alice_verifying_key=enacted_policy.publisher_verifying_key,
)
actual_exception = reencrypt_http_spy.spy_exception
assert type(actual_exception) == middleware_exception_class # be specific
# verify message is not in bytes form
assert "b'" not in str(actual_exception) # no byte string included in message
assert str(exception_parameter) in str(actual_exception)

View File

@ -0,0 +1,56 @@
from http import HTTPStatus
from urllib.parse import urlparse
import pytest
from tests.utils.middleware import MockRestMiddleware
@pytest.mark.parametrize(
"status_code, expected_exception_class",
[
(HTTPStatus.BAD_REQUEST, MockRestMiddleware.BadRequest),
(HTTPStatus.NOT_FOUND, MockRestMiddleware.NotFound),
(HTTPStatus.PAYMENT_REQUIRED, MockRestMiddleware.PaymentRequired),
(HTTPStatus.FORBIDDEN, MockRestMiddleware.Unauthorized),
# catch alls
(HTTPStatus.REQUEST_TIMEOUT, MockRestMiddleware.UnexpectedResponse),
(HTTPStatus.INTERNAL_SERVER_ERROR, MockRestMiddleware.UnexpectedResponse),
(HTTPStatus.NOT_IMPLEMENTED, MockRestMiddleware.UnexpectedResponse),
(HTTPStatus.SERVICE_UNAVAILABLE, MockRestMiddleware.UnexpectedResponse),
],
)
def test_middleware_response_status_code_processing(
status_code,
expected_exception_class,
mocker,
mock_rest_middleware,
ursulas,
):
ursula = list(ursulas)[0]
_original_execute_method = mock_rest_middleware.client._execute_method
def execute_method_side_effect(
node_or_sprout, host, port, method, endpoint, *args, **kwargs
):
endpoint_url = urlparse(endpoint)
if endpoint_url.path == "/reencrypt":
response = mocker.MagicMock(
text="I have not failed. I've just found 10,000 ways that won't work.", # -- Thomas Edison
status_code=status_code,
)
return response
else:
return _original_execute_method(
node_or_sprout, host, port, method, endpoint, *args, **kwargs
)
mocker.patch.object(
mock_rest_middleware.client,
"_execute_method",
side_effect=execute_method_side_effect,
)
with pytest.raises(expected_exception_class):
mock_rest_middleware.reencrypt(
ursula=ursula, reencryption_request_bytes=b"reencryption_request"
)