Skip to content

Commit

Permalink
feat(#163): Headless processing of replay files.
Browse files Browse the repository at this point in the history
  • Loading branch information
alxbl committed Mar 17, 2020
1 parent c743b90 commit 3ae2a68
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 9 deletions.
21 changes: 16 additions & 5 deletions bin/pyrdp-player.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,22 @@ def main():
logging.error('Headless mode is not specified and PySide2 is not installed. Install PySide2 to use the graphical user interface.')
exit(127)

app = QApplication(sys.argv)
mainWindow = MainWindow(args.bind, int(args.port), args.replay)
mainWindow.show()

return app.exec_()
if not args.headless:
app = QApplication(sys.argv)
mainWindow = MainWindow(args.bind, int(args.port), args.replay)
mainWindow.show()

return app.exec_()
else:
logging.info('Starting PyRDP Player in headless mode.')
from pyrdp.player import HeadlessEventHandler
from pyrdp.player import Replay
processEvents = HeadlessEventHandler()
for replay in args.replay:
processEvents.output.write(f'== REPLAY FILE: {replay}\n')
fd = open(replay, "rb")
replay = Replay(fd, handler=processEvents)
processEvents.output.write('\n-- END --------------------------------\n')


if __name__ == '__main__':
Expand Down
154 changes: 154 additions & 0 deletions pyrdp/player/HeadlessEventHandler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2020 GoSecure Inc.
# Licensed under the GPLv3 or later.
#


from io import TextIOBase
from sys import stdout

from pyrdp.logging import log
from pyrdp.enum import DeviceType, KeyboardFlag, ParserMode, PlayerPDUType
from pyrdp.parser import BasicFastPathParser, ClientConnectionParser, ClientInfoParser, ClipboardParser, SlowPathParser
from pyrdp.pdu import FastPathScanCodeEvent, FastPathUnicodeEvent, FormatDataResponsePDU, InputPDU, KeyboardEvent, FastPathMouseEvent, MouseEvent, \
PlayerDeviceMappingPDU, PlayerPDU
from pyrdp.player import keyboard
from pyrdp.core import decodeUTF16LE, Observer


class HeadlessEventHandler(Observer):
"""
Handle events from a replay file without rendering to a surface.
This event handler does not require any graphical dependencies.
"""

def __init__(self, output: TextIOBase = stdout):
super().__init__()
self.output = output

self.shiftPressed = False
self.capsLockOn = False
self.buffer = b""

# Instantiate parsers.
self.slowpath = SlowPathParser()
# self.fastpath = FastPathOutputParser()
self.clipboard = ClipboardParser()

self.handlers = {
PlayerPDUType.CLIENT_DATA: self.onClientData,
PlayerPDUType.CLIENT_INFO: self.onClientInfo,
PlayerPDUType.CONNECTION_CLOSE: self.onConnectionClose,
PlayerPDUType.CLIPBOARD_DATA: self.onClipboardData,
PlayerPDUType.SLOW_PATH_PDU: self.onSlowPathPDU,
PlayerPDUType.FAST_PATH_INPUT: self.onFastPathInput,
PlayerPDUType.DEVICE_MAPPING: self.onDeviceMapping,
}

def writeText(self, text: str):
self.output.write(text.rstrip("\x00"))

def writeSeparator(self):
self.output.write("\n--------------------\n")

def onPDUReceived(self, pdu: PlayerPDU, isMainThread=False):
log.debug("Received %(pdu)s", {"pdu": pdu})
if pdu.header in self.handlers:
self.handlers[pdu.header](pdu)

def onClientData(self, pdu: PlayerPDU):
parser = ClientConnectionParser()
clientDataPDU = parser.parse(pdu.payload)
clientName = clientDataPDU.coreData.clientName.strip("\x00")

self.writeSeparator()
self.writeText(f"HOST: {clientName}\n")
self.writeSeparator()

def onClientInfo(self, pdu: PlayerPDU):
parser = ClientInfoParser()
clientInfoPDU = parser.parse(pdu.payload)

self.writeSeparator()

self.writeText("USERNAME: {}\nPASSWORD: {}\nDOMAIN: {}\n".format(
clientInfoPDU.username.replace("\x00", ""),
clientInfoPDU.password.replace("\x00", ""),
clientInfoPDU.domain.replace("\x00", "")
))

self.writeSeparator()

def onConnectionClose(self, _: PlayerPDU):
self.writeText("\n<Connection closed>")

def onClipboardData(self, pdu: PlayerPDU):
parser = ClipboardParser()
pdu = parser.parse(pdu.payload)

if not isinstance(pdu, FormatDataResponsePDU):
return

clipboardData = decodeUTF16LE(pdu.requestedFormatData)

self.writeSeparator()
self.writeText(f"CLIPBOARD DATA: {clipboardData}")
self.writeSeparator()

def onSlowPathPDU(self, pdu: PlayerPDU):
pdu = self.slowpath.parse(pdu.payload)

if not isinstance(pdu, InputPDU):
return
for event in pdu.events:
if isinstance(event, MouseEvent):
self.onMousePosition(event.x, event.y)
elif isinstance(event, KeyboardEvent):
down = event.flags & KeyboardFlag.KBDFLAGS_DOWN == 0
ext = event.flags & KeyboardFlag.KBDFLAGS_EXTENDED != 0
self.onScanCode(event.keyCode, down, ext)

def onFastPathInput(self, pdu: PlayerPDU):
parser = BasicFastPathParser(ParserMode.SERVER)
pdu = parser.parse(pdu.payload)

for event in pdu.events:
if isinstance(event, FastPathUnicodeEvent):
if not event.released:
self.onUnicode(event)
elif isinstance(event, FastPathMouseEvent):
self.onMouse(event)
elif isinstance(event, FastPathScanCodeEvent):
ext = event.rawHeaderByte & keyboard.KBDFLAGS_EXTENDED != 0
self.onScanCode(event.scanCode, event.isReleased, ext)

def onUnicode(self, event: FastPathUnicodeEvent):
self.writeText(str(event.text))

def onMouse(self, event: FastPathMouseEvent):
self.onMousePosition(event.mouseX, event.mouseY)

def onMousePosition(self, x: int, y: int):
pass

def onScanCode(self, scanCode: int, isReleased: bool, isExtended: bool):
keyName = keyboard.getKeyName(scanCode, isExtended, self.shiftPressed, self.capsLockOn)

if len(keyName) == 1:
if not isReleased:
self.writeText(keyName)
else:
self.writeText(f"\n<{keyName} {'released' if isReleased else 'pressed'}>")

# Handle shift.
if scanCode in [0x2A, 0x36]:
self.shiftPressed = not isReleased

# Caps lock
elif scanCode == 0x3A and not isReleased:
self.capsLockOn = not self.capsLockOn

def onDeviceMapping(self, pdu: PlayerDeviceMappingPDU):
self.writeText(f"\n<{DeviceType.getPrettyName(pdu.deviceType)} mapped: {pdu.name}>")
14 changes: 10 additions & 4 deletions pyrdp/player/Replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@

from pyrdp.layer import PlayerLayer
from pyrdp.pdu import PlayerPDU
from pyrdp.core import Observer


class Replay:
"""
Class containing information on a replay's events.
"""

def __init__(self, file: BinaryIO):
def __init__(self, file: BinaryIO, handler: Observer = None):
self.events: Dict[int, List[int]] = {}

# Remember the current file position
Expand All @@ -36,9 +37,14 @@ def __init__(self, file: BinaryIO):
def registerEvent(pdu: PlayerPDU):
events[pdu.timestamp].append(currentMessagePosition)

# The layer will take care of parsing for us
player = PlayerLayer()
player.createObserver(onPDUReceived = registerEvent)
if not handler:
# Register the offset of every event in the file.
player.createObserver(onPDUReceived=registerEvent)
else:
# A handler for events was provided, eagerly process
# all events.
player.addObserver(handler)

# Parse all events in the file
while file.tell() < size:
Expand All @@ -63,4 +69,4 @@ def registerEvent(pdu: PlayerPDU):
relativeTimestamp = absoluteTimestamp - referenceTime
self.events[relativeTimestamp] = events[absoluteTimestamp]

self.duration = (timestamps[-1] - referenceTime) / 1000.0
self.duration = (timestamps[-1] - referenceTime) / 1000.0
1 change: 1 addition & 0 deletions pyrdp/player/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from pyrdp.player.LiveWindow import LiveWindow
from pyrdp.player.MainWindow import MainWindow
from pyrdp.player.PlayerEventHandler import PlayerEventHandler
from pyrdp.player.HeadlessEventHandler import HeadlessEventHandler
from pyrdp.player.QTimerSequencer import QTimerSequencer
from pyrdp.player.Replay import Replay
from pyrdp.player.ReplayBar import ReplayBar
Expand Down

0 comments on commit 3ae2a68

Please sign in to comment.