diff --git a/README.md b/README.md index 0c525fa..9010bac 100644 --- a/README.md +++ b/README.md @@ -301,8 +301,6 @@ If you are a viewer developer, please put them in a viewer. * AISv3 wrapper? * Higher level wrappers for common things? I don't really need these, so only if people want to write them. -* Highlight matched portion of message in log view, if applicable -* * Remember deep filters and return a map of them, have message formatter return text ranges? * Move things out of `templates.py`, right now most binary serialization stuff lives there because it's more convenient for me to hot-reload. * Ability to add menus? diff --git a/hippolyzer/apps/proxy_gui.py b/hippolyzer/apps/proxy_gui.py index d105ec4..26e1d52 100644 --- a/hippolyzer/apps/proxy_gui.py +++ b/hippolyzer/apps/proxy_gui.py @@ -35,7 +35,7 @@ from hippolyzer.lib.proxy.ca_utils import setup_ca_everywhere from hippolyzer.lib.proxy.caps_client import CapsClient from hippolyzer.lib.proxy.http_proxy import create_proxy_master, HTTPFlowContext from hippolyzer.lib.proxy.packets import Direction -from hippolyzer.lib.proxy.message import ProxiedMessage, VerbatimHumanVal, proxy_eval +from hippolyzer.lib.proxy.message import ProxiedMessage, VerbatimHumanVal, proxy_eval, SpannedString from hippolyzer.lib.proxy.message_logger import LLUDPMessageLogEntry, AbstractMessageLogEntry from hippolyzer.lib.proxy.region import ProxiedRegion from hippolyzer.lib.proxy.sessions import Session, SessionManager @@ -161,6 +161,8 @@ class ProxyGUI(QtWidgets.QMainWindow): "ViewerAsset GetTexture SetAlwaysRun GetDisplayNames MapImageService MapItemReply".split(" ") DEFAULT_FILTER = f"!({' || '.join(ignored for ignored in DEFAULT_IGNORE)})" + textRequest: QtWidgets.QTextEdit + def __init__(self): super().__init__() loadUi(MAIN_WINDOW_UI_PATH, self) @@ -263,6 +265,12 @@ class ProxyGUI(QtWidgets.QMainWindow): beautify=self.checkBeautify.isChecked(), replacements=self.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) resp = entry.response(beautify=self.checkBeautify.isChecked()) self.textRequest.setPlainText(req) if resp: @@ -271,6 +279,13 @@ class ProxyGUI(QtWidgets.QMainWindow): else: self.textResponse.hide() + if highlight_range: + cursor = self.textRequest.textCursor() + cursor.setPosition(highlight_range[0], QtGui.QTextCursor.KeepAnchor) + highlight_format = QtGui.QTextBlockFormat() + highlight_format.setBackground(QtCore.Qt.yellow) + cursor.setBlockFormat(highlight_format) + def beforeInsert(self): vbar = self.tableView.verticalScrollBar() self._shouldScrollOnInsert = vbar.value() == vbar.maximum() diff --git a/hippolyzer/lib/proxy/message.py b/hippolyzer/lib/proxy/message.py index f21622b..b29c8fe 100644 --- a/hippolyzer/lib/proxy/message.py +++ b/hippolyzer/lib/proxy/message.py @@ -5,6 +5,7 @@ import logging import math import os import re +import typing import uuid from typing import * @@ -71,6 +72,14 @@ def proxy_eval(eval_str: str, globals_=None, locals_=None): ) +TextSpan = Tuple[int, int] +SpanDict = Dict[Tuple[Union[str, int], ...], TextSpan] + + +class SpannedString(str): + spans: SpanDict = {} + + class ProxiedMessage(Message): __slots__ = ("meta", "injected", "dropped", "direction") @@ -83,9 +92,10 @@ class ProxiedMessage(Message): _maybe_reload_templates() def to_human_string(self, replacements=None, beautify=False, - template: Optional[MessageTemplate] = None): + template: Optional[MessageTemplate] = None) -> SpannedString: replacements = replacements or {} _maybe_reload_templates() + spans: SpanDict = {} string = "" if self.direction is not None: string += f'{self.direction.name} ' @@ -101,11 +111,17 @@ class ProxiedMessage(Message): block_suffix = "" if template and template.get_block(block_name).block_type == MsgBlockType.MBT_VARIABLE: block_suffix = ' # Variable' - for block in block_list: + for block_num, block in enumerate(block_list): string += f"[{block_name}]{block_suffix}\n" for var_name, val in block.items(): + start_len = len(string) string += self._format_var(block, var_name, val, replacements, beautify) - return string + end_len = len(string) + # Store the spans for each var so we can highlight specific matches + spans[(self.name, block_name, block_num, var_name)] = (start_len, end_len) + spanned = SpannedString(string) + spanned.spans = spans + return spanned def _format_var(self, block, var_name, var_val, replacements, beautify=False): string = "" diff --git a/hippolyzer/lib/proxy/message_filter.py b/hippolyzer/lib/proxy/message_filter.py index 666878e..4777204 100644 --- a/hippolyzer/lib/proxy/message_filter.py +++ b/hippolyzer/lib/proxy/message_filter.py @@ -62,9 +62,12 @@ def message_filter(): return expression, EOF +MATCH_RESULT = typing.Union[bool, typing.Tuple] + + class BaseFilterNode(abc.ABC): @abc.abstractmethod - def match(self, msg) -> bool: + def match(self, msg) -> MATCH_RESULT: raise NotImplementedError() @property @@ -94,17 +97,17 @@ class BinaryFilterNode(BaseFilterNode, abc.ABC): class UnaryNotFilterNode(UnaryFilterNode): - def match(self, msg) -> bool: + def match(self, msg) -> MATCH_RESULT: return not self.node.match(msg) class OrFilterNode(BinaryFilterNode): - def match(self, msg) -> bool: + def match(self, msg) -> MATCH_RESULT: return self.left_node.match(msg) or self.right_node.match(msg) class AndFilterNode(BinaryFilterNode): - def match(self, msg) -> bool: + def match(self, msg) -> MATCH_RESULT: return self.left_node.match(msg) and self.right_node.match(msg) @@ -114,7 +117,7 @@ class MessageFilterNode(BaseFilterNode): self.operator = operator self.value = value - def match(self, msg) -> bool: + def match(self, msg) -> MATCH_RESULT: return msg.matches(self) @property diff --git a/hippolyzer/lib/proxy/message_logger.py b/hippolyzer/lib/proxy/message_logger.py index 7501b8a..45266f1 100644 --- a/hippolyzer/lib/proxy/message_logger.py +++ b/hippolyzer/lib/proxy/message_logger.py @@ -586,15 +586,19 @@ class LLUDPMessageLogEntry(AbstractMessageLogEntry): for block_name in message.blocks: if not fnmatch.fnmatchcase(block_name, matcher.selector[1]): continue - for block in message[block_name]: + for block_num, block in enumerate(message[block_name]): for var_name in block.vars.keys(): 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) if selector_len == 3: + # We're just matching on the var existing, not having any particular value if matcher.value is None: - return True + return span_key if self._val_matches(matcher.operator, block[var_name], matcher.value): - return True + return span_key + # Need to invoke a special unpacker elif selector_len == 4: try: deserialized = block.deserialize_var(var_name) @@ -608,9 +612,9 @@ class LLUDPMessageLogEntry(AbstractMessageLogEntry): for key in deserialized.keys(): if fnmatch.fnmatchcase(str(key), matcher.selector[3]): if matcher.value is None: - return True + return span_key if self._val_matches(matcher.operator, deserialized[key], matcher.value): - return True + return span_key return False