From afc333ab4966f74f8f33e4b7b79d2c33204e50b7 Mon Sep 17 00:00:00 2001 From: Salad Dais Date: Wed, 8 Dec 2021 23:50:16 +0000 Subject: [PATCH] Improve highlighting of matched fields in message log --- hippolyzer/apps/proxy_gui.py | 27 ++++++++++--------- hippolyzer/lib/proxy/message_filter.py | 36 +++++++++++++++++++------- hippolyzer/lib/proxy/message_logger.py | 30 +++++++++++---------- 3 files changed, 56 insertions(+), 37 deletions(-) diff --git a/hippolyzer/apps/proxy_gui.py b/hippolyzer/apps/proxy_gui.py index 5641f46..ac1032a 100644 --- a/hippolyzer/apps/proxy_gui.py +++ b/hippolyzer/apps/proxy_gui.py @@ -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: diff --git a/hippolyzer/lib/proxy/message_filter.py b/hippolyzer/lib/proxy/message_filter.py index b1952ba..28e05b3 100644 --- a/hippolyzer/lib/proxy/message_filter.py +++ b/hippolyzer/lib/proxy/message_filter.py @@ -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 diff --git a/hippolyzer/lib/proxy/message_logger.py b/hippolyzer/lib/proxy/message_logger.py index f540275..2e89798 100644 --- a/hippolyzer/lib/proxy/message_logger.py +++ b/hippolyzer/lib/proxy/message_logger.py @@ -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):