diff --git a/bin/pyrdp-player.py b/bin/pyrdp-player.py index 6fc5c9307..225385966 100755 --- a/bin/pyrdp-player.py +++ b/bin/pyrdp-player.py @@ -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__': diff --git a/pyrdp/player/HeadlessEventHandler.py b/pyrdp/player/HeadlessEventHandler.py new file mode 100644 index 000000000..ed19ccf1f --- /dev/null +++ b/pyrdp/player/HeadlessEventHandler.py @@ -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") + + 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}>") diff --git a/pyrdp/player/Replay.py b/pyrdp/player/Replay.py index 1c9d6e429..fc1df9b82 100644 --- a/pyrdp/player/Replay.py +++ b/pyrdp/player/Replay.py @@ -10,6 +10,7 @@ from pyrdp.layer import PlayerLayer from pyrdp.pdu import PlayerPDU +from pyrdp.core import Observer class Replay: @@ -17,7 +18,7 @@ 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 @@ -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: @@ -63,4 +69,4 @@ def registerEvent(pdu: PlayerPDU): relativeTimestamp = absoluteTimestamp - referenceTime self.events[relativeTimestamp] = events[absoluteTimestamp] - self.duration = (timestamps[-1] - referenceTime) / 1000.0 \ No newline at end of file + self.duration = (timestamps[-1] - referenceTime) / 1000.0 diff --git a/pyrdp/player/__init__.py b/pyrdp/player/__init__.py index 784c81325..94b5dac6c 100644 --- a/pyrdp/player/__init__.py +++ b/pyrdp/player/__init__.py @@ -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