Files
Hippolyzer/hippolyzer/apps/proxy_gui.py

957 lines
38 KiB
Python
Raw Normal View History

2021-04-30 17:30:24 +00:00
import asyncio
import base64
import dataclasses
2021-04-30 17:30:24 +00:00
import email
import functools
import html
import itertools
import json
import logging
import pathlib
import multiprocessing
import re
import signal
import socket
import sys
import urllib.parse
from typing import *
import multidict
from qasync import QEventLoop, asyncSlot
from PySide6 import QtCore, QtWidgets, QtGui
2021-04-30 17:30:24 +00:00
from hippolyzer.apps.model import MessageLogModel, MessageLogHeader, RegionListModel
from hippolyzer.apps.proxy import start_proxy
2021-04-30 17:30:24 +00:00
from hippolyzer.lib.base import llsd
from hippolyzer.lib.base.datatypes import UUID
from hippolyzer.lib.base.helpers import bytes_unescape, bytes_escape, get_resource_filename, create_logged_task
2021-04-30 17:30:24 +00:00
from hippolyzer.lib.base.message.llsd_msg_serializer import LLSDMessageSerializer
from hippolyzer.lib.base.message.message import Block, Message
from hippolyzer.lib.base.message.message_formatting import (
HumanMessageSerializer,
VerbatimHumanVal,
subfield_eval,
SpannedString,
)
2021-04-30 17:30:24 +00:00
from hippolyzer.lib.base.message.msgtypes import MsgType
2021-06-17 21:28:22 +00:00
from hippolyzer.lib.base.message.template_dict import DEFAULT_TEMPLATE_DICT
from hippolyzer.lib.base.settings import SettingDescriptor
2021-04-30 17:30:24 +00:00
from hippolyzer.lib.base.ui_helpers import loadUi
import hippolyzer.lib.base.serialization as se
from hippolyzer.lib.base.network.transport import Direction, SocketUDPTransport
2023-12-10 23:26:28 +00:00
from hippolyzer.lib.client.state import BaseClientSessionManager
2021-04-30 17:30:24 +00:00
from hippolyzer.lib.proxy.addons import BaseInteractionManager, AddonManager
from hippolyzer.lib.proxy.ca_utils import setup_ca_everywhere
from hippolyzer.lib.proxy.caps_client import ProxyCapsClient
from hippolyzer.lib.proxy.http_proxy import create_http_proxy, HTTPFlowContext
from hippolyzer.lib.proxy.message_logger import LLUDPMessageLogEntry, AbstractMessageLogEntry, WrappingMessageLogger, \
import_log_entries, export_log_entries
2021-04-30 17:30:24 +00:00
from hippolyzer.lib.proxy.region import ProxiedRegion
from hippolyzer.lib.proxy.sessions import Session, SessionManager
from hippolyzer.lib.proxy.settings import ProxySettings
2021-04-30 17:30:24 +00:00
from hippolyzer.lib.proxy.templates import CAP_TEMPLATES
LOG = logging.getLogger(__name__)
MAIN_WINDOW_UI_PATH = get_resource_filename("apps/proxy_mainwindow.ui")
MESSAGE_BUILDER_UI_PATH = get_resource_filename("apps/message_builder.ui")
ADDON_DIALOG_UI_PATH = get_resource_filename("apps/addon_dialog.ui")
FILTER_DIALOG_UI_PATH = get_resource_filename("apps/filter_dialog.ui")
2021-04-30 17:30:24 +00:00
def show_error_message(error_msg, parent=None):
error_dialog = QtWidgets.QErrorMessage(parent=parent)
# No obvious way to set this to plaintext, yuck...
error_dialog.showMessage(html.escape(error_msg))
2021-12-09 00:00:48 +00:00
error_dialog.exec()
2021-04-30 17:30:24 +00:00
error_dialog.raise_()
class GUISessionManager(SessionManager, QtCore.QObject):
regionAdded = QtCore.Signal(ProxiedRegion)
regionRemoved = QtCore.Signal(ProxiedRegion)
def __init__(self, settings):
2023-12-10 23:26:28 +00:00
BaseClientSessionManager.__init__(self)
SessionManager.__init__(self, settings)
2021-04-30 17:30:24 +00:00
QtCore.QObject.__init__(self)
self.all_regions = []
self.message_logger = WrappingMessageLogger()
2021-04-30 17:30:24 +00:00
def checkRegions(self):
new_regions = itertools.chain(*[s.regions for s in self.sessions])
new_regions = [r for r in new_regions if r.is_alive]
for new_region in new_regions:
if new_region not in self.all_regions:
self.regionAdded.emit(new_region) # type: ignore
for old_region in self.all_regions:
if old_region not in new_regions:
self.regionRemoved.emit(old_region) # type: ignore
self.all_regions = new_regions
2021-12-09 00:00:48 +00:00
class GUIInteractionManager(BaseInteractionManager):
def __init__(self, parent: QtWidgets.QWidget):
2021-04-30 17:30:24 +00:00
BaseInteractionManager.__init__(self)
2021-12-09 00:00:48 +00:00
self._parent = parent
2021-04-30 17:30:24 +00:00
def main_window_handle(self) -> Any:
2021-12-09 00:00:48 +00:00
return self._parent
2021-04-30 17:30:24 +00:00
def _dialog_async_exec(self, dialog: QtWidgets.QDialog):
future = asyncio.Future()
dialog.finished.connect(lambda r: future.set_result(r))
dialog.open()
return future
async def _file_dialog(
self, caption: str, directory: str, filter_str: str, mode: QtWidgets.QFileDialog.FileMode,
default_suffix: str = '',
) -> Tuple[bool, QtWidgets.QFileDialog]:
2021-12-09 00:00:48 +00:00
dialog = QtWidgets.QFileDialog(self._parent, caption=caption, directory=directory, filter=filter_str)
2021-04-30 17:30:24 +00:00
dialog.setFileMode(mode)
if mode == QtWidgets.QFileDialog.FileMode.AnyFile:
dialog.setAcceptMode(QtWidgets.QFileDialog.AcceptMode.AcceptSave)
if default_suffix:
dialog.setDefaultSuffix(default_suffix)
2021-04-30 17:30:24 +00:00
res = await self._dialog_async_exec(dialog)
return res, dialog
async def open_files(self, caption: str = '', directory: str = '', filter_str: str = '') -> List[str]:
res, dialog = await self._file_dialog(
caption, directory, filter_str, QtWidgets.QFileDialog.FileMode.ExistingFiles
)
if not res:
return []
return dialog.selectedFiles()
async def open_file(self, caption: str = '', directory: str = '', filter_str: str = '') -> Optional[str]:
res, dialog = await self._file_dialog(
caption, directory, filter_str, QtWidgets.QFileDialog.FileMode.ExistingFile
)
if not res:
return None
return dialog.selectedFiles()[0]
async def open_dir(self, caption: str = '', directory: str = '', filter_str: str = '') -> Optional[str]:
res, dialog = await self._file_dialog(
caption, directory, filter_str, QtWidgets.QFileDialog.FileMode.Directory
)
if not res:
return None
return dialog.selectedFiles()[0]
async def save_file(self, caption: str = '', directory: str = '', filter_str: str = '',
default_suffix: str = '') -> Optional[str]:
2021-04-30 17:30:24 +00:00
res, dialog = await self._file_dialog(
caption, directory, filter_str, QtWidgets.QFileDialog.FileMode.AnyFile, default_suffix,
2021-04-30 17:30:24 +00:00
)
if not res or not dialog.selectedFiles():
return None
return dialog.selectedFiles()[0]
async def confirm(self, title: str, caption: str) -> bool:
msg = QtWidgets.QMessageBox(
QtWidgets.QMessageBox.Icon.Question,
title,
caption,
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
2021-12-09 00:00:48 +00:00
self._parent,
)
fut = asyncio.Future()
msg.finished.connect(lambda r: fut.set_result(r))
msg.open()
return (await fut) == QtWidgets.QMessageBox.Ok
2021-04-30 17:30:24 +00:00
class GUIProxySettings(ProxySettings):
FIRST_RUN: bool = SettingDescriptor(True)
"""Persistent settings backed by QSettings"""
def __init__(self, settings: QtCore.QSettings):
super().__init__()
self._settings_obj = settings
def get_setting(self, name: str) -> Any:
val: Any = self._settings_obj.value(name, defaultValue=dataclasses.MISSING)
if val is dataclasses.MISSING:
return val
return json.loads(val)
def set_setting(self, name: str, val: Any):
self._settings_obj.setValue(name, json.dumps(val))
2021-04-30 17:30:24 +00:00
def nonFatalExceptions(f):
@functools.wraps(f)
def _wrapper(self, *args, **kwargs):
try:
return f(self, *args, **kwargs)
except Exception as e:
logging.exception("Treating exception as non-fatal")
show_error_message(str(e), parent=self)
return None
return _wrapper
def buildReplacements(session: Session, region: ProxiedRegion):
if not session or not region:
return {}
selected = session.selected
agent_object = region.objects.lookup_fullid(session.agent_id)
selected_local = selected.object_local
selected_object = None
if selected_local:
# We may or may not have an object for this
selected_object = region.objects.lookup_localid(selected_local)
return {
"SELECTED_LOCAL": selected_local,
"SELECTED_FULL": selected_object.FullID if selected_object else None,
"SELECTED_PARCEL_LOCAL": selected.parcel_local,
"SELECTED_PARCEL_FULL": selected.parcel_full,
"SELECTED_SCRIPT_ITEM": selected.script_item,
"SELECTED_TASK_ITEM": selected.task_item,
"AGENT_ID": session.agent_id,
"AGENT_LOCAL": agent_object.LocalID if agent_object else None,
"SESSION_ID": session.id,
"AGENT_POS": agent_object.Position if agent_object else None,
"NULL_KEY": UUID(),
"RANDOM_KEY": UUID.random,
"CIRCUIT_CODE": session.circuit_code,
"REGION_HANDLE": region.handle,
}
class MessageLogWindow(QtWidgets.QMainWindow):
2021-04-30 17:30:24 +00:00
DEFAULT_IGNORE = "StartPingCheck CompletePingCheck PacketAck SimulatorViewerTimeMessage SimStats " \
"AgentUpdate AgentAnimation AvatarAnimation ViewerEffect CoarseLocationUpdate LayerData " \
"CameraConstraint ObjectUpdateCached RequestMultipleObjects ObjectUpdate ObjectUpdateCompressed " \
"ImprovedTerseObjectUpdate KillObject ImagePacket SoundTrigger AttachedSound PreloadSound " \
"GetMesh GetMesh2 EventQueueGet RequestObjectPropertiesFamily ObjectPropertiesFamily " \
"AvatarRenderInfo FirestormBridge ObjectAnimation ParcelDwellRequest ParcelAccessListRequest " \
"ParcelDwellReply ParcelAccessListReply AttachedSoundGainChange " \
"ParcelPropertiesRequest ParcelProperties GetObjectCost GetObjectPhysicsData ObjectImage " \
"ViewerAsset GetTexture SetAlwaysRun GetDisplayNames MapImageService MapItemReply " \
"AgentFOV GenericStreamingMessage".split(" ")
2021-04-30 17:30:24 +00:00
DEFAULT_FILTER = f"!({' || '.join(ignored for ignored in DEFAULT_IGNORE)})"
textRequest: QtWidgets.QTextEdit
def __init__(
self, settings: GUIProxySettings, session_manager: GUISessionManager,
log_live_messages: bool, parent: Optional[QtWidgets.QWidget] = None,
):
super().__init__(parent=parent)
2021-04-30 17:30:24 +00:00
loadUi(MAIN_WINDOW_UI_PATH, self)
if parent:
self.setWindowTitle("Message Log")
self.menuBar.setEnabled(False) # type: ignore
self.menuBar.hide() # type: ignore
2021-04-30 17:30:24 +00:00
self._selectedEntry: Optional[AbstractMessageLogEntry] = None
self.settings = settings
self.sessionManager = session_manager
if log_live_messages:
self.model = MessageLogModel(parent=self.tableView)
session_manager.message_logger.loggers.append(self.model)
else:
self.model = MessageLogModel(parent=self.tableView, maxlen=None)
2021-04-30 17:30:24 +00:00
self.tableView.setModel(self.model)
self.model.rowsAboutToBeInserted.connect(self.beforeInsert)
self.model.rowsInserted.connect(self.afterInsert)
self.tableView.selectionModel().selectionChanged.connect(self._messageSelected)
self.checkBeautify.clicked.connect(self._showSelectedMessage)
self.checkPause.clicked.connect(self._setPaused)
self.setFilter(self.DEFAULT_FILTER)
2021-04-30 17:30:24 +00:00
self.btnClearLog.clicked.connect(self.model.clear)
self.lineEditFilter.editingFinished.connect(self.setFilter)
2021-04-30 17:30:24 +00:00
self.btnMessageBuilder.clicked.connect(self._sendToMessageBuilder)
self.btnCopyRepr.clicked.connect(self._copyRepr)
self.actionInstallHTTPSCerts.triggered.connect(self.installHTTPSCerts)
2021-04-30 17:30:24 +00:00
self.actionManageAddons.triggered.connect(self._manageAddons)
self.actionManageFilters.triggered.connect(self._manageFilters)
self.actionOpenMessageBuilder.triggered.connect(self._openMessageBuilder)
self.actionProxyRemotelyAccessible.setChecked(self.settings.REMOTELY_ACCESSIBLE)
self.actionProxySSLInsecure.setChecked(self.settings.SSL_INSECURE)
self.actionUseViewerObjectCache.setChecked(self.settings.USE_VIEWER_OBJECT_CACHE)
self.actionRequestMissingObjects.setChecked(self.settings.AUTOMATICALLY_REQUEST_MISSING_OBJECTS)
2021-04-30 17:30:24 +00:00
self.actionProxyRemotelyAccessible.triggered.connect(self._setProxyRemotelyAccessible)
self.actionProxySSLInsecure.triggered.connect(self._setProxySSLInsecure)
self.actionUseViewerObjectCache.triggered.connect(self._setUseViewerObjectCache)
self.actionRequestMissingObjects.triggered.connect(self._setRequestMissingObjects)
self.actionOpenNewMessageLogWindow.triggered.connect(self._openNewMessageLogWindow)
self.actionImportLogEntries.triggered.connect(self._importLogEntries)
self.actionExportLogEntries.triggered.connect(self._exportLogEntries)
2021-04-30 17:30:24 +00:00
self._filterMenu = QtWidgets.QMenu()
self._populateFilterMenu()
self.toolButtonFilter.setMenu(self._filterMenu)
self._shouldScrollOnInsert = True
self.tableView.horizontalHeader().resizeSection(MessageLogHeader.Host, 80)
self.tableView.horizontalHeader().resizeSection(MessageLogHeader.Method, 60)
self.tableView.horizontalHeader().resizeSection(MessageLogHeader.Name, 180)
self.tableView.horizontalHeader().setStretchLastSection(True)
self.textResponse.hide()
def closeEvent(self, event) -> None:
loggers = self.sessionManager.message_logger.loggers
if self.model in loggers:
loggers.remove(self.model)
super().closeEvent(event)
2021-04-30 17:30:24 +00:00
def _populateFilterMenu(self):
def _addFilterAction(text, filter_str):
filter_action = QtGui.QAction(text, self)
filter_action.triggered.connect(lambda: self.setFilter(filter_str))
2021-04-30 17:30:24 +00:00
self._filterMenu.addAction(filter_action)
self._filterMenu.clear()
_addFilterAction("Default", self.DEFAULT_FILTER)
filters = self.settings.FILTERS
2021-04-30 17:30:24 +00:00
for preset_name, preset_filter in filters.items():
_addFilterAction(preset_name, preset_filter)
2021-11-19 04:30:36 +00:00
def getFilterDict(self):
return self.settings.FILTERS
2021-04-30 17:30:24 +00:00
def setFilterDict(self, val: dict):
self.settings.FILTERS = val
2021-04-30 17:30:24 +00:00
self._populateFilterMenu()
def _manageFilters(self):
dialog = FilterDialog(self)
2021-12-09 00:00:48 +00:00
dialog.exec()
2021-04-30 17:30:24 +00:00
@nonFatalExceptions
def setFilter(self, filter_str=None):
2021-04-30 17:30:24 +00:00
if filter_str is None:
filter_str = self.lineEditFilter.text()
else:
self.lineEditFilter.setText(filter_str)
self.model.set_filter(filter_str)
2021-04-30 17:30:24 +00:00
def _setPaused(self, checked):
self.model.set_paused(checked)
2021-04-30 17:30:24 +00:00
def _messageSelected(self, selected, _deselected):
indexes = selected.indexes()
if not len(indexes):
self.btnMessageBuilder.setEnabled(False)
self.btnCopyRepr.setEnabled(False)
return
index = indexes[0]
entry: AbstractMessageLogEntry = index.data(QtCore.Qt.UserRole)
self._selectedEntry = entry
self.btnMessageBuilder.setEnabled(True)
if isinstance(entry, LLUDPMessageLogEntry):
self.btnCopyRepr.setEnabled(True)
else:
self.btnCopyRepr.setEnabled(False)
self._showSelectedMessage()
def _showSelectedMessage(self):
entry = self._selectedEntry
if not entry:
return
req = entry.request(
beautify=self.checkBeautify.isChecked(),
replacements=buildReplacements(entry.session, entry.region),
2021-04-30 17:30:24 +00:00
)
2021-05-08 01:23:00 +00:00
self.textRequest.setPlainText(req)
# 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, short_circuit=False).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)
2021-05-08 01:23:00 +00:00
resp = entry.response(beautify=self.checkBeautify.isChecked())
if resp:
self.textResponse.show()
self.textResponse.setPlainText(resp)
else:
self.textResponse.hide()
2021-04-30 17:30:24 +00:00
def beforeInsert(self):
vbar = self.tableView.verticalScrollBar()
self._shouldScrollOnInsert = vbar.value() == vbar.maximum()
def afterInsert(self):
if self._shouldScrollOnInsert:
self.tableView.scrollToBottom()
def _sendToMessageBuilder(self):
if not self._selectedEntry:
return
win = MessageBuilderWindow(self, self.sessionManager)
win.show()
msg = self._selectedEntry
beautify = self.checkBeautify.isChecked()
replacements = buildReplacements(msg.session, msg.region)
2021-04-30 17:30:24 +00:00
win.setMessageText(msg.request(beautify=beautify, replacements=replacements))
@nonFatalExceptions
def _copyRepr(self):
if not self._selectedEntry:
return
if not isinstance(self._selectedEntry, LLUDPMessageLogEntry):
return
msg_repr = self._selectedEntry.message.repr(pretty=True)
QtGui.QGuiApplication.clipboard().setText(msg_repr)
def _openMessageBuilder(self):
win = MessageBuilderWindow(self, self.sessionManager)
win.show()
def _openNewMessageLogWindow(self):
win: QtWidgets.QMainWindow = MessageLogWindow(
self.settings, self.sessionManager, log_live_messages=True, parent=self)
win.setFilter(self.lineEditFilter.text())
win.show()
win.activateWindow()
@asyncSlot()
async def _importLogEntries(self):
log_file = await AddonManager.UI.open_file(
caption="Import Log Entries", filter_str="Hippolyzer Logs (*.hippolog)"
)
if not log_file:
return
win = MessageLogWindow(self.settings, self.sessionManager, log_live_messages=False, parent=self)
win.setFilter(self.lineEditFilter.text())
with open(log_file, "rb") as f:
entries = import_log_entries(f.read())
for entry in entries:
win.model.add_log_entry(entry)
win.show()
win.activateWindow()
@asyncSlot()
async def _exportLogEntries(self):
log_file = await AddonManager.UI.save_file(
caption="Export Log Entries", filter_str="Hippolyzer Logs (*.hippolog)", default_suffix="hippolog",
)
if not log_file:
return
with open(log_file, "wb") as f:
f.write(export_log_entries(self.model))
def installHTTPSCerts(self):
2021-04-30 17:30:24 +00:00
msg = QtWidgets.QMessageBox()
msg.setText("Would you like to install the proxy's HTTPS certificate in the config dir"
" of any installed viewers so that HTTPS connections will work?")
2021-04-30 17:30:24 +00:00
yes_btn = msg.addButton("Yes", QtWidgets.QMessageBox.NoRole)
msg.addButton("No", QtWidgets.QMessageBox.NoRole)
msg.exec()
clicked_btn = msg.clickedButton()
if clicked_btn is not yes_btn:
return
master = create_http_proxy("127.0.0.1", -1, HTTPFlowContext())
2021-04-30 17:30:24 +00:00
dirs = setup_ca_everywhere(master)
msg = QtWidgets.QMessageBox()
if dirs:
msg.setText(f"Installed certificate in {[str(x) for x in dirs]!r}, restart your viewer!")
else:
msg.setText("Couldn't find any viewer config directories!")
msg.exec()
def _setProxyRemotelyAccessible(self, checked: bool):
self.sessionManager.settings.REMOTELY_ACCESSIBLE = checked
2021-04-30 17:30:24 +00:00
msg = QtWidgets.QMessageBox()
msg.setText("Remote accessibility setting changes will take effect on next run")
msg.exec()
def _setProxySSLInsecure(self, checked: bool):
self.sessionManager.settings.SSL_INSECURE = checked
msg = QtWidgets.QMessageBox()
msg.setText("SSL security setting changes will take effect on next run")
msg.exec()
def _setUseViewerObjectCache(self, checked: bool):
self.sessionManager.settings.USE_VIEWER_OBJECT_CACHE = checked
def _setRequestMissingObjects(self, checked: bool):
self.sessionManager.settings.AUTOMATICALLY_REQUEST_MISSING_OBJECTS = checked
2021-04-30 17:30:24 +00:00
def _manageAddons(self):
dialog = AddonDialog(self)
2021-12-09 00:00:48 +00:00
dialog.exec()
2021-04-30 17:30:24 +00:00
def getAddonList(self) -> List[str]:
return self.sessionManager.settings.ADDON_SCRIPTS
2021-04-30 17:30:24 +00:00
def setAddonList(self, val: List[str]):
self.sessionManager.settings.ADDON_SCRIPTS = val
2021-04-30 17:30:24 +00:00
BANNED_HEADERS = ("content-length", "host")
def _parse_http_message(msg_text):
request_line, mime_msg = re.split(r"\r?\n", msg_text, 1)
method, uri, version = msg_text.split(" ", 2)
# comment line for URI, throw it out.
while mime_msg.startswith("#"):
_, mime_msg = re.split(r"\r?\n", mime_msg, 1)
msg = email.message_from_string(mime_msg)
# Throw out Content-Length, very likely to be wrong after beautification
headers = multidict.CIMultiDict(
(k, v) for k, v in msg.items() if k.lower() not in BANNED_HEADERS
)
# Use to get the body rather than `email.` since that wants to do some
# fancy parsing of MIME bits that we don't want it to do.
msg_parts = re.split(r"\r?\n\r?\n", msg_text, 1)
body = None
if len(msg_parts) == 2:
body = msg_parts[1]
return method, uri, headers, body
def _coerce_to_bytes(val):
if isinstance(val, bytes):
return val
if not isinstance(val, str):
val = str(val)
return val.encode("utf8")
class MessageBuilderWindow(QtWidgets.QMainWindow):
def __init__(self, parent, session_manager):
super().__init__(parent=parent)
loadUi(MESSAGE_BUILDER_UI_PATH, self)
2021-06-17 21:28:22 +00:00
self.templateDict = DEFAULT_TEMPLATE_DICT
2021-04-30 17:30:24 +00:00
self.llsdSerializer = LLSDMessageSerializer()
self.sessionManager: SessionManager = session_manager
self.regionModel = RegionListModel(self, self.sessionManager)
self.listRegions.setModel(self.regionModel)
self._populateMessageTypeMenus()
self.comboTrusted.textActivated.connect(self._fillUDPMessage)
self.comboUntrusted.textActivated.connect(self._fillUDPMessage)
self.comboCaps.textActivated.connect(self._fillCapsMessage)
self.btnSend.clicked.connect(self._sendMessage)
self.textRequest.setFocus(QtCore.Qt.FocusReason.OtherFocusReason)
def _getTarget(self):
region_indexes = self.listRegions.selectionModel().selectedIndexes()
session, region = None, None
if region_indexes:
region = region_indexes[0].data(QtCore.Qt.UserRole)
session = region.session()
elif self.sessionManager.sessions:
session = self.sessionManager.sessions[-1]
region = session.main_region
return session, region
def setMessageText(self, text):
self.textRequest.setPlainText(text)
def _populateMessageTypeMenus(self):
self.comboTrusted.clear()
self.comboUntrusted.clear()
self.comboCaps.clear()
self.comboTrusted.addItem("<Trusted>")
self.comboUntrusted.addItem("<Untrusted>")
self.comboCaps.addItem("<Caps>")
message_names = sorted(x.name for x in self.templateDict)
for message_name in message_names:
2024-01-04 19:08:09 +00:00
if self.templateDict[message_name].trusted:
2021-04-30 17:30:24 +00:00
self.comboTrusted.addItem(message_name)
else:
self.comboUntrusted.addItem(message_name)
cap_names = sorted(set(itertools.chain(*[r.cap_urls.keys() for r in self.regionModel.regions])))
2021-04-30 17:30:24 +00:00
for cap_name in cap_names:
if cap_name.endswith("ProxyWrapper"):
continue
self.comboCaps.addItem(cap_name)
def _fillCapsMessage(self, cap_name):
if cap_name.startswith("<"):
return
session, region = self._getTarget()
self.textRequest.clear()
params = ""
path = ""
body = "<llsd><undef/></llsd>"
method = "POST"
headers = "Content-Type: application/llsd+xml\nX-Hippo-Directives: 1\n"
for cap_template in CAP_TEMPLATES:
# Might be nice to allow selecting method variants?
if cap_template.cap_name == cap_name:
params = urllib.parse.urlencode({x: '' for x in cap_template.query})
if params:
params = '?' + params
body = cap_template.body.decode("utf8")
path = cap_template.path
method = cap_template.method
if method == "GET":
# No content-type because no content
headers = ""
break
self.textRequest.setPlainText(
f"""{method} [[{cap_name}]]{path}{params} HTTP/1.1
# {region.cap_urls.get(cap_name, "<unknown URI>")}
2021-04-30 17:30:24 +00:00
{headers}
{body}"""
)
def _fillUDPMessage(self, message_name):
if message_name.startswith("<"):
return
self.textRequest.clear()
template = self.templateDict[message_name]
msg = Message(message_name, direction=Direction.OUT)
2021-04-30 17:30:24 +00:00
for tmpl_block in template.blocks:
num_blocks = tmpl_block.number or 1
for i in range(num_blocks):
fill_vars = {}
for var in tmpl_block.variables:
fill_vars[var.name] = self._getVarPlaceholder(template, tmpl_block, var)
msg_block = Block(tmpl_block.name, **fill_vars)
msg.add_block(msg_block)
self.textRequest.setPlainText(
HumanMessageSerializer.to_human_string(msg, replacements={}, beautify=True, template=template)
2021-04-30 17:30:24 +00:00
)
def _getVarPlaceholder(self, msg, block, var):
if block.name == "AgentData":
if var.name == "AgentID":
return VerbatimHumanVal("[[AGENT_ID]]")
if var.name == "SessionID":
return VerbatimHumanVal("[[SESSION_ID]]")
if block.name == "ObjectData":
if var.name.endswith("ID") and var.name != "AgentID":
if var.type == MsgType.MVT_LLUUID:
return VerbatimHumanVal("[[SELECTED_FULL]]")
else:
return VerbatimHumanVal("[[SELECTED_LOCAL]]")
if "Parcel" in msg.name:
if var.name.endswith("LocalID"):
return VerbatimHumanVal("[[SELECTED_PARCEL_LOCAL]]")
if "ParcelID" in var.name:
return VerbatimHumanVal("[[SELECTED_PARCEL_FULL]]")
if "Handle" in var.name:
return VerbatimHumanVal("[[REGION_HANDLE]]")
if "CircuitCode" in var.name or ("Circuit" in block.name and "Code" in var.name):
return VerbatimHumanVal("[[CIRCUIT_CODE]]")
if var.name.endswith("LocalID"):
return VerbatimHumanVal("[[SELECTED_LOCAL]]")
if any(var.name.endswith(x) for x in ("TransactionID", "TransferID", "Invoice", "QueryID")):
return VerbatimHumanVal("[[RANDOM_KEY]]")
if var.name in ("TaskID", "ObjectID"):
return VerbatimHumanVal("[[SELECTED_FULL]]")
default_val = var.default_value
if default_val is not None:
return default_val
2021-04-30 17:30:24 +00:00
return VerbatimHumanVal("")
@nonFatalExceptions
def _sendMessage(self, _checked=False):
session, region = self._getTarget()
msg_text = self.textRequest.toPlainText()
replacements = buildReplacements(session, region)
2021-04-30 17:30:24 +00:00
if re.match(r"\A\s*(in|out)\s+", msg_text, re.I):
sender_func = self._sendLLUDPMessage
elif re.match(r"\A\s*(eq)\s+", msg_text, re.I):
sender_func = self._sendEQMessage
2021-04-30 17:30:24 +00:00
elif re.match(r"\A.*http/[0-9.]+\r?\n", msg_text, re.I):
sender_func = self._sendHTTPMessage
else:
raise ValueError("IDK what kind of message %s is" % msg_text)
for i in range(0, self.spinRepeat.value() + 1):
sender_func(session, region, msg_text, {"i": i, **replacements})
def _buildEnv(self, session, region):
env = {}
if region:
env["full_to_local"] = lambda x: region.objects.lookup_fullid(x).LocalID
env["region"] = region
if session:
env["session"] = session
return env
def _sendLLUDPMessage(self, session, region: Optional[ProxiedRegion], msg_text, replacements):
if not session or not region:
raise RuntimeError("Need a valid session and region to send LLUDP")
env = self._buildEnv(session, region)
# We specifically want to allow `eval()` in messages since
# messages from here are trusted.
msg = HumanMessageSerializer.from_human_string(msg_text, replacements, env, safe=False)
2021-04-30 17:30:24 +00:00
if self.checkLLUDPViaCaps.isChecked():
if msg.direction == Direction.IN:
region.eq_manager.inject_message(msg)
2021-04-30 17:30:24 +00:00
else:
self._sendHTTPRequest(
"POST",
region.cap_urls["UntrustedSimulatorMessage"],
2021-04-30 17:30:24 +00:00
{"Content-Type": "application/llsd+xml", "Accept": "application/llsd+xml"},
self.llsdSerializer.serialize(msg),
)
else:
transport = None
off_circuit = self.checkOffCircuit.isChecked()
if off_circuit:
transport = SocketUDPTransport(socket.socket(socket.AF_INET, socket.SOCK_DGRAM))
2021-12-09 05:30:12 +00:00
region.circuit.send(msg, transport=transport)
if off_circuit:
transport.close()
2021-04-30 17:30:24 +00:00
def _sendEQMessage(self, session, region: Optional[ProxiedRegion], msg_text: str, replacements: dict):
if not session or not region:
raise RuntimeError("Need a valid session and region to send EQ event")
message_line, _, body = (x.strip() for x in msg_text.partition("\n"))
message_name = message_line.rsplit(" ", 1)[-1]
env = self._buildEnv(session, region)
def directive_handler(m):
return self._handleHTTPDirective(env, replacements, False, m)
body = re.sub(rb"<!HIPPO(\w+)\[\[(.*?)]]>", directive_handler, body.encode("utf8"), flags=re.S)
region.eq_manager.inject_event({
"message": message_name,
"body": llsd.parse_xml(body),
})
2021-04-30 17:30:24 +00:00
def _sendHTTPMessage(self, session, region, msg_text: str, replacements: dict):
env = self._buildEnv(session, region)
method, uri, headers, body = _parse_http_message(msg_text)
has_directives = headers.get("X-Hippo-Directives") == "1"
is_escaped = headers.get("X-Hippo-Escaped-Body") == "1"
is_beautified = headers.get("X-Hippo-Beautify") == "1"
if body:
body = body.encode("utf8")
# Only handle directives if specifically asked for
if has_directives:
def directive_handler(m):
return self._handleHTTPDirective(env, replacements, is_escaped, m)
body = re.sub(rb"<!HIPPO(\w+)\[\[(.*?)]]>", directive_handler, body, flags=re.S)
match = re.match(r"\A\[\[(.*?)]](.*)", uri)
cap_name = None
if match:
cap_name = match.group(1)
cap_url = session.global_caps.get(cap_name)
if not cap_url:
cap_url = region.cap_urls.get(cap_name)
2021-04-30 17:30:24 +00:00
if not cap_url:
raise ValueError("Don't have a Cap for %s" % cap_name)
uri = cap_url + match.group(2)
# The body contains python escape sequences, decode them.
if is_escaped:
body = bytes_unescape(body)
# Convert from human-friendly representation before sending
if cap_name and is_beautified:
parsed = llsd.parse_xml(body)
serializer = se.HTTP_SERIALIZERS.get(cap_name)
if serializer:
body = serializer.serialize_req_body(method, parsed)
if body is se.UNSERIALIZABLE:
raise ValueError(f"Cannot serialize {parsed!r}")
headers.pop("X-Hippo-Beautify", None)
headers.pop("X-Hippo-Directives", None)
headers.pop("X-Hippo-Escaped-Body", None)
self._sendHTTPRequest(method, uri, headers, body)
def _handleHTTPDirective(self, env: dict, replacements: dict, need_escaped: bool, match: re.Match) -> bytes:
# HTTP messages from the builder can have inline directives like
# <!HIPPOBASE64[[foobar]]>, handle those.
directive: bytes = match.group(1)
contents: bytes = match.group(2)
unescaped_contents: bytes = bytes_unescape(contents)
if directive == b"BASE64":
val = base64.b64encode(unescaped_contents)
elif directive == b"CDATA":
val = contents.replace(b"&", b"&amp;").replace(b"<", b"&lt;").replace(b">", b"&gt;")
elif directive == b"UNESCAPE":
val = unescaped_contents
elif directive == b"EVAL":
val = subfield_eval(contents.decode("utf8").strip(), globals_={**env, **replacements})
2021-04-30 17:30:24 +00:00
val = _coerce_to_bytes(val)
elif directive == b"REPL":
repl = replacements[contents.decode("utf8").strip()]
if callable(repl):
repl = repl()
val = _coerce_to_bytes(repl)
2021-04-30 17:30:24 +00:00
else:
raise ValueError(f"Unknown directive {directive}")
# We're going to output this in a context that expects string escape sequences
# This isn't very nice, but saves me writing a parser for the language in HTTP
# request bodies.
if need_escaped:
return bytes_escape(val)
return val
def _sendHTTPRequest(self, method, uri, headers, body):
caps_client = ProxyCapsClient(self.sessionManager.settings)
2021-04-30 17:30:24 +00:00
async def _send_request():
req = caps_client.request(method, uri, headers=headers, data=body)
async with req as resp:
# Read, but throw away the resp so the connection is kept alive long
# enough for the full response to pass through the proxy
await resp.read()
create_logged_task(_send_request(), "Send HTTP Request")
2021-04-30 17:30:24 +00:00
class AddonDialog(QtWidgets.QDialog):
listAddons: QtWidgets.QListWidget
def __init__(self, parent: MessageLogWindow):
2021-04-30 17:30:24 +00:00
super().__init__()
loadUi(ADDON_DIALOG_UI_PATH, self)
self._parent = parent
self.btnLoad.pressed.connect(self._loadPressed)
self.btnRemove.pressed.connect(self._removeCurrentPressed)
self._populateAddons()
def _populateAddons(self):
for addon in self._parent.getAddonList():
self.listAddons.addItem(QtWidgets.QListWidgetItem(addon))
@nonFatalExceptions
def _removeCurrentPressed(self):
selected = self.listAddons.selectedItems()
if selected:
item: QtWidgets.QListWidgetItem = selected[0]
try:
AddonManager.unload_addon_from_path(item.text(), reload=True)
except Exception as e:
# This addon might not even really be loaded
show_error_message(error_msg=str(e), parent=self)
addons = self._parent.getAddonList()
addons.remove(item.text())
self._parent.setAddonList(addons)
idx = self.listAddons.indexFromItem(item).row()
self.listAddons.takeItem(idx)
@nonFatalExceptions
def _loadPressed(self):
file_filter = 'Python Addon (*.py)'
options = QtWidgets.QFileDialog.Options()
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, 'Load', '', file_filter, options=options)
if not path:
return
addons = self._parent.getAddonList()
path = str(pathlib.Path(path).absolute())
# already loaded
if path in addons:
return
AddonManager.load_addon_from_path(path, reload=True)
addons.append(path)
self.listAddons.addItem(QtWidgets.QListWidgetItem(path))
self._parent.setAddonList(addons)
class FilterDialog(QtWidgets.QDialog):
listFilters: QtWidgets.QListWidget
def __init__(self, parent: MessageLogWindow):
2021-04-30 17:30:24 +00:00
super().__init__()
loadUi(FILTER_DIALOG_UI_PATH, self)
self._parent = parent
self.btnSaveCurrent.pressed.connect(self._saveCurrent)
self.btnRemove.pressed.connect(self._removeCurrentPressed)
self._populateFilters()
def _populateFilters(self):
self.listFilters.clear()
for filter_obj in self._parent.getFilterDict():
self.listFilters.addItem(QtWidgets.QListWidgetItem(filter_obj))
@nonFatalExceptions
def _saveCurrent(self):
filters = self._parent.getFilterDict()
filter_text = self._parent.lineEditFilter.text()
name, ok = QtWidgets.QInputDialog.getText(self, "Filter Name", "Filter Name",
QtWidgets.QLineEdit.EchoMode.Normal)
if not name or not ok:
return
filters[name] = filter_text
self._parent.setFilterDict(filters)
self._populateFilters()
@nonFatalExceptions
def _removeCurrentPressed(self):
selected = self.listFilters.selectedItems()
if selected:
item: QtWidgets.QListWidgetItem = selected[0]
filters = self._parent.getFilterDict()
del filters[item.text()]
self._parent.setFilterDict(filters)
idx = self.listFilters.indexFromItem(item).row()
self.listFilters.takeItem(idx)
def gui_main():
multiprocessing.set_start_method('spawn')
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
app = QtWidgets.QApplication(sys.argv)
loop = QEventLoop(app)
asyncio.set_event_loop(loop)
settings = GUIProxySettings(QtCore.QSettings("SaladDais", "hippolyzer"))
session_manager = GUISessionManager(settings)
window = MessageLogWindow(settings, session_manager, log_live_messages=True)
AddonManager.UI = GUIInteractionManager(window)
2021-04-30 17:30:24 +00:00
timer = QtCore.QTimer(app)
timer.timeout.connect(window.sessionManager.checkRegions)
timer.start(100)
signal.signal(signal.SIGINT, lambda *args: QtWidgets.QApplication.quit())
window.show()
http_host = None
if window.sessionManager.settings.REMOTELY_ACCESSIBLE:
2021-04-30 17:30:24 +00:00
http_host = "0.0.0.0"
if settings.FIRST_RUN:
settings.FIRST_RUN = False
# Automatically offer to install the HTTPS certs on first run.
window.installHTTPSCerts()
start_proxy(
2021-04-30 17:30:24 +00:00
session_manager=window.sessionManager,
extra_addon_paths=window.getAddonList(),
proxy_host=http_host,
ssl_insecure=settings.SSL_INSECURE,
2021-04-30 17:30:24 +00:00
)
2021-05-03 17:28:42 +00:00
if __name__ == "__main__":
multiprocessing.freeze_support()
gui_main()