Improve highlighting of matched fields in message log
This commit is contained in:
@@ -360,21 +360,20 @@ class MessageLogWindow(QtWidgets.QMainWindow):
|
||||
beautify=self.checkBeautify.isChecked(),
|
||||
replacements=buildReplacements(entry.session, entry.region),
|
||||
)
|
||||
highlight_range = None
|
||||
if isinstance(req, SpannedString):
|
||||
match_result = self.model.filter.match(entry)
|
||||
# Match result was a tuple indicating what matched
|
||||
if isinstance(match_result, tuple):
|
||||
highlight_range = req.spans.get(match_result)
|
||||
|
||||
self.textRequest.setPlainText(req)
|
||||
if highlight_range:
|
||||
cursor = self.textRequest.textCursor()
|
||||
cursor.setPosition(highlight_range[0], QtGui.QTextCursor.MoveAnchor)
|
||||
cursor.setPosition(highlight_range[1], QtGui.QTextCursor.KeepAnchor)
|
||||
highlight_format = QtGui.QTextBlockFormat()
|
||||
highlight_format.setBackground(QtCore.Qt.yellow)
|
||||
cursor.setBlockFormat(highlight_format)
|
||||
# The string has a map of fields and their associated positions within the string,
|
||||
# use that to highlight any individual fields the filter matched on.
|
||||
if isinstance(req, SpannedString):
|
||||
for field in self.model.filter.match(entry).fields:
|
||||
field_span = req.spans.get(field)
|
||||
if not field_span:
|
||||
continue
|
||||
cursor = self.textRequest.textCursor()
|
||||
cursor.setPosition(field_span[0], QtGui.QTextCursor.MoveAnchor)
|
||||
cursor.setPosition(field_span[1], QtGui.QTextCursor.KeepAnchor)
|
||||
highlight_format = QtGui.QTextBlockFormat()
|
||||
highlight_format.setBackground(QtCore.Qt.yellow)
|
||||
cursor.setBlockFormat(highlight_format)
|
||||
|
||||
resp = entry.response(beautify=self.checkBeautify.isChecked())
|
||||
if resp:
|
||||
|
||||
@@ -69,12 +69,17 @@ def message_filter():
|
||||
return expression, EOF
|
||||
|
||||
|
||||
MATCH_RESULT = typing.Union[bool, typing.Tuple]
|
||||
class MatchResult(typing.NamedTuple):
|
||||
result: bool
|
||||
fields: typing.List[typing.Tuple]
|
||||
|
||||
def __bool__(self):
|
||||
return self.result
|
||||
|
||||
|
||||
class BaseFilterNode(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def match(self, msg) -> MATCH_RESULT:
|
||||
def match(self, msg) -> MatchResult:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
@@ -104,18 +109,31 @@ class BinaryFilterNode(BaseFilterNode, abc.ABC):
|
||||
|
||||
|
||||
class UnaryNotFilterNode(UnaryFilterNode):
|
||||
def match(self, msg) -> MATCH_RESULT:
|
||||
return not self.node.match(msg)
|
||||
def match(self, msg) -> MatchResult:
|
||||
# Should we pass fields up here? Maybe not.
|
||||
return MatchResult(not self.node.match(msg), [])
|
||||
|
||||
|
||||
class OrFilterNode(BinaryFilterNode):
|
||||
def match(self, msg) -> MATCH_RESULT:
|
||||
return self.left_node.match(msg) or self.right_node.match(msg)
|
||||
def match(self, msg) -> MatchResult:
|
||||
left_match = self.left_node.match(msg)
|
||||
if left_match:
|
||||
return MatchResult(True, left_match.fields)
|
||||
right_match = self.right_node.match(msg)
|
||||
if right_match:
|
||||
return MatchResult(True, right_match.fields)
|
||||
return MatchResult(False, [])
|
||||
|
||||
|
||||
class AndFilterNode(BinaryFilterNode):
|
||||
def match(self, msg) -> MATCH_RESULT:
|
||||
return self.left_node.match(msg) and self.right_node.match(msg)
|
||||
def match(self, msg) -> MatchResult:
|
||||
left_match = self.left_node.match(msg)
|
||||
if not left_match:
|
||||
return MatchResult(False, [])
|
||||
right_match = self.right_node.match(msg)
|
||||
if not right_match:
|
||||
return MatchResult(False, [])
|
||||
return MatchResult(True, left_match.fields + right_match.fields)
|
||||
|
||||
|
||||
class MessageFilterNode(BaseFilterNode):
|
||||
@@ -124,7 +142,7 @@ class MessageFilterNode(BaseFilterNode):
|
||||
self.operator = operator
|
||||
self.value = value
|
||||
|
||||
def match(self, msg) -> MATCH_RESULT:
|
||||
def match(self, msg) -> MatchResult:
|
||||
return msg.matches(self)
|
||||
|
||||
@property
|
||||
|
||||
@@ -21,7 +21,7 @@ from hippolyzer.lib.base.datatypes import TaggedUnion, UUID, TupleCoord
|
||||
from hippolyzer.lib.base.helpers import bytes_escape
|
||||
from hippolyzer.lib.base.message.message_formatting import HumanMessageSerializer
|
||||
from hippolyzer.lib.proxy.message_filter import MetaFieldSpecifier, compile_filter, BaseFilterNode, MessageFilterNode, \
|
||||
EnumFieldSpecifier
|
||||
EnumFieldSpecifier, MatchResult
|
||||
from hippolyzer.lib.proxy.http_flow import HippoHTTPFlow
|
||||
from hippolyzer.lib.proxy.caps import CapType, SerializedCapData
|
||||
|
||||
@@ -366,8 +366,8 @@ class AbstractMessageLogEntry(abc.ABC):
|
||||
return self._val_matches(matcher.operator, self._get_meta(matcher.selector[1]), matcher.value)
|
||||
return None
|
||||
|
||||
def matches(self, matcher: "MessageFilterNode"):
|
||||
return self._base_matches(matcher) or False
|
||||
def matches(self, matcher: "MessageFilterNode") -> "MatchResult":
|
||||
return MatchResult(self._base_matches(matcher) or False, [])
|
||||
|
||||
@property
|
||||
def seq(self):
|
||||
@@ -671,20 +671,20 @@ class LLUDPMessageLogEntry(AbstractMessageLogEntry):
|
||||
def request(self, beautify=False, replacements=None):
|
||||
return HumanMessageSerializer.to_human_string(self.message, replacements, beautify)
|
||||
|
||||
def matches(self, matcher):
|
||||
def matches(self, matcher) -> "MatchResult":
|
||||
base_matched = self._base_matches(matcher)
|
||||
if base_matched is not None:
|
||||
return base_matched
|
||||
return MatchResult(base_matched, [])
|
||||
|
||||
if not self._packet_root_matches(matcher.selector[0]):
|
||||
return False
|
||||
return MatchResult(False, [])
|
||||
|
||||
message = self.message
|
||||
|
||||
selector_len = len(matcher.selector)
|
||||
# name, block_name, var_name(, subfield_name)?
|
||||
if selector_len not in (3, 4):
|
||||
return False
|
||||
return MatchResult(False, [])
|
||||
for block_name in message.blocks:
|
||||
if not fnmatch.fnmatchcase(block_name, matcher.selector[1]):
|
||||
continue
|
||||
@@ -693,13 +693,15 @@ class LLUDPMessageLogEntry(AbstractMessageLogEntry):
|
||||
if not fnmatch.fnmatchcase(var_name, matcher.selector[2]):
|
||||
continue
|
||||
# So we know where the match happened
|
||||
span_key = (message.name, block_name, block_num, var_name)
|
||||
field_key = (message.name, block_name, block_num, var_name)
|
||||
if selector_len == 3:
|
||||
# We're just matching on the var existing, not having any particular value
|
||||
if matcher.value is None:
|
||||
return span_key
|
||||
# TODO: Ability to disable short-circuiting when matching for display
|
||||
# purposes, it's helpful to see every match in the message.
|
||||
return MatchResult(True, [field_key])
|
||||
if self._val_matches(matcher.operator, block[var_name], matcher.value):
|
||||
return span_key
|
||||
return MatchResult(True, [field_key])
|
||||
# Need to invoke a special unpacker
|
||||
elif selector_len == 4:
|
||||
try:
|
||||
@@ -710,15 +712,15 @@ class LLUDPMessageLogEntry(AbstractMessageLogEntry):
|
||||
if isinstance(deserialized, TaggedUnion):
|
||||
deserialized = deserialized.value
|
||||
if not isinstance(deserialized, dict):
|
||||
return False
|
||||
return MatchResult(False, [])
|
||||
for key in deserialized.keys():
|
||||
if fnmatch.fnmatchcase(str(key), matcher.selector[3]):
|
||||
if matcher.value is None:
|
||||
return span_key
|
||||
return MatchResult(True, [field_key])
|
||||
if self._val_matches(matcher.operator, deserialized[key], matcher.value):
|
||||
return span_key
|
||||
return MatchResult(True, [field_key])
|
||||
|
||||
return False
|
||||
return MatchResult(False, [])
|
||||
|
||||
@property
|
||||
def summary(self):
|
||||
|
||||
Reference in New Issue
Block a user