Skip to content

Commit

Permalink
Merge pull request #190 from GoSecure/headless-mode
Browse files Browse the repository at this point in the history
Multiple Player improvements
  • Loading branch information
obilodeau authored Mar 24, 2020
2 parents 4d43dee + 791726d commit 0c7f6ac
Show file tree
Hide file tree
Showing 14 changed files with 428 additions and 65 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,16 @@ Finally, you can install the project with Pip:

```
pip3 install -U pip setuptools wheel
# Without GUI dependencies
pip3 install -U -e .
# With GUI dependencies
pip3 install -U -e '.[GUI]'
```

This should install all the dependencies required to run PyRDP.
This should install the dependencies required to run PyRDP. If you choose to install without GUI dependencies,
it will not be possible to use `pyrdp-player` without headless mode (`--headless`)

If you ever want to leave your virtual environment, you can simply deactivate it:

Expand Down
68 changes: 47 additions & 21 deletions bin/pyrdp-player.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,33 @@

#
# This file is part of the PyRDP project.
# Copyright (C) 2018, 2019 GoSecure Inc.
# Copyright (C) 2018-2020 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

import asyncio

# asyncio needs to be imported first to ensure that the reactor is
# installed properly. Do not re-order.
import asyncio
from twisted.internet import asyncioreactor

asyncioreactor.install(asyncio.get_event_loop())

from pyrdp.player import HAS_GUI
from pyrdp.logging import LOGGER_NAMES, NotifyHandler

from pathlib import Path
import argparse
import logging
import logging.handlers
import sys
import os

from PySide2.QtWidgets import QApplication
if HAS_GUI:
from pyrdp.player import MainWindow
from PySide2.QtWidgets import QApplication

from pyrdp.logging import LOGGER_NAMES, NotifyHandler
from pyrdp.player import MainWindow

def prepareLoggers(logLevel: int, outDir: Path):
def prepareLoggers(logLevel: int, outDir: Path, headless: bool):
logDir = outDir / "logs"
logDir.mkdir(exist_ok = True)

Expand All @@ -42,15 +46,21 @@ def prepareLoggers(logLevel: int, outDir: Path):
pyrdpLogger.addHandler(fileHandler)
pyrdpLogger.setLevel(logLevel)

# https://docs.python.org/3/library/os.html
if os.name != "nt":
notifyHandler = NotifyHandler()
notifyHandler.setFormatter(notificationFormatter)

uiLogger = logging.getLogger(LOGGER_NAMES.PLAYER_UI)
uiLogger.addHandler(notifyHandler)
else:
pyrdpLogger.warning("Notifications are not supported for your platform, they will be disabled.")
if not headless and HAS_GUI:
# https://docs.python.org/3/library/os.html
if os.name != "nt":
try:
notifyHandler = NotifyHandler()
notifyHandler.setFormatter(notificationFormatter)

uiLogger = logging.getLogger(LOGGER_NAMES.PLAYER_UI)
uiLogger.addHandler(notifyHandler)
except Exception:
# No notification daemon or DBus, can't use notifications.
pass
else:
pyrdpLogger.warning("Notifications are not supported for your platform, they will be disabled.")
return pyrdpLogger

def main():
"""
Expand All @@ -63,19 +73,35 @@ def main():
parser.add_argument("-p", "--port", help="Bind port (default: 3000)", default=3000)
parser.add_argument("-o", "--output", help="Output folder", default="pyrdp_output")
parser.add_argument("-L", "--log-level", help="Log level", default="INFO", choices=["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"], nargs="?")
parser.add_argument("--headless", help="Parse a replay without rendering the user interface.", action="store_true")

args = parser.parse_args()
outDir = Path(args.output)
outDir.mkdir(exist_ok = True)

logLevel = getattr(logging, args.log_level)
prepareLoggers(logLevel, outDir)
logger = prepareLoggers(logLevel, outDir, args.headless)

app = QApplication(sys.argv)
mainWindow = MainWindow(args.bind, int(args.port), args.replay)
mainWindow.show()
if not HAS_GUI and not args.headless:
logger.error('Headless mode is not specified and PySide2 is not installed. Install PySide2 to use the graphical user interface.')
sys.exit(127)

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:
logger.info('Starting PyRDP Player in headless mode.')
from pyrdp.player import HeadlessEventHandler
from pyrdp.player.Replay 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
138 changes: 137 additions & 1 deletion pyrdp/enum/scancode.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@

ScanCodeTuple = namedtuple("ScanCode", "code extended")

# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/089d362b-31eb-4a1a-b6fa-92fe61bb5dbf
KBDFLAGS_EXTENDED = 2


class ScanCode:
"""
Enumeration for RDP scan codes. Values are a tuple of (scanCode: int, isExtended: bool).
Expand Down Expand Up @@ -181,4 +185,136 @@ class ScanCode:
BROWSER_SEARCH = ScanCodeTuple(0x65, True) # VK_BROWSER_SEARCH
BROWSER_FAVORITES = ScanCodeTuple(0x66, True) # VK_BROWSER_FAVORITES
BROWSER_HOME = ScanCodeTuple(0x32, True) # VK_BROWSER_HOME
LAUNCH_MAIL = ScanCodeTuple(0x6C, True) # VK_LAUNCH_MAIL
LAUNCH_MAIL = ScanCodeTuple(0x6C, True) # VK_LAUNCH_MAIL


"""
Scancode to key name mapping.
Each scancode is an array containing the variant when pressed in
position 0 and the variant when shift is being held in position 1.
# Example
```python
assert SCANCODE_NAMES[0x10][0] == 'q'
assert SCANCODE_NAMES[0x10][1] == 'Q'
```
For scancodes that do not have a different a different name,
`scancode[0] == scancode[1]`
"""
SCANCODE_NAMES = {
0x01: ['Escape', 'Escape'],
0x02: ['1', '!'],
0x03: ['2', '@'],
0x04: ['3', '#'],
0x05: ['4', '$'],
0x06: ['5', '%'],
0x07: ['6', '^'],
0x08: ['7', '&'],
0x09: ['8', '*'],
0x0A: ['9', '('],
0x0B: ['0', ')'],
0x0C: ['-', '_'],
0x0D: ['=', '+'],
0x0E: ['Backspace', 'Backspace'],
0x0F: ['Tab', 'Tab'],
0x10: ['q', 'Q'],
0x11: ['w', 'W'],
0x12: ['e', 'E'],
0x13: ['r', 'R'],
0x14: ['t', 'T'],
0x15: ['y', 'Y'],
0x16: ['u', 'U'],
0x17: ['i', 'I'],
0x18: ['o', 'O'],
0x19: ['p', 'P'],
0x1A: ['[', '{'],
0x1B: [']', '}'],
0x1C: ['Return', 'Return'],
0x1D: ['Control', 'Control'],
0x1E: ['a', 'A'],
0x1F: ['s', 'S'],
0x20: ['d', 'D'],
0x21: ['f', 'F'],
0x22: ['g', 'G'],
0x23: ['h', 'H'],
0x24: ['j', 'J'],
0x25: ['k', 'K'],
0x26: ['l', 'L'],
0x27: [';', ':'],
0x28: ["'", '"'],
0x29: ['`', '~'],
0x2A: ['Shift', 'Shift'],
0x2B: ['\\', '|'],
0x2C: ['z', 'Z'],
0x2D: ['x', 'X'],
0x2E: ['c', 'C'],
0x2F: ['v', 'V'],
0x30: ['b', 'B'],
0x31: ['n', 'N'],
0x32: ['m', 'M'],
0x33: [',', '<'],
0x34: ['.', '>'],
0x35: ['/', '?'],
0x36: ['Shift', 'Shift'],
0x38: ['Alt', 'AltGr'],
0x39: ['Space', 'Space'],
0x3A: ['CapsLock', 'CapsLock'],
0x3B: ['F1', 'F1'],
0x3C: ['F2', 'F2'],
0x3D: ['F3', 'F3'],
0x3E: ['F4', 'F4'],
0x3F: ['F5', 'F5'],
0x40: ['F6', 'F6'],
0x41: ['F7', 'F7'],
0x42: ['F8', 'F8'],
0x43: ['F9', 'F9'],
0x44: ['F10', 'F10'],
0x45: ['NumLock', 'NumLock'],
0x46: ['ScrollLock', 'ScrollLock'],
0x47: ['Home', 'Home'],
0x48: ['Up', 'Up'],
0x49: ['PageUp', 'PageUp'],
0x4B: ['Left', 'Left'],
0x4D: ['Right', 'Right'],
0x4F: ['End', 'End'],
0x50: ['Down', 'Down'],
0x51: ['PageDown', 'PageDown'],
0x52: ['Insert', 'Insert'],
0x53: ['Delete', 'Delete'],
0x54: ['SysReq', 'SysReq'],
0x57: ['F11', 'F11'],
0x58: ['F12', 'F12'],
0x5B: ['Meta', 'Meta'],
0x5D: ['Menu', 'Menu'],
0x5F: ['Sleep', 'Sleep'],
0x62: ['Zoom', 'Zoom'],
0x63: ['Help', 'Help'],
0x64: ['F13', 'F13'],
0x65: ['F14', 'F14'],
0x66: ['F15', 'F15'],
0x67: ['F16', 'F16'],
0x68: ['F17', 'F17'],
0x69: ['F18', 'F18'],
0x6A: ['F19', 'F19'],
0x6B: ['F20', 'F20'],
0x6C: ['F21', 'F21'],
0x6D: ['F22', 'F22'],
0x6E: ['F23', 'F23'],
0x6F: ['F24', 'F24'],
0x70: ['Hiragana', 'Hiragana'],
0x71: ['Kanji', 'Kanji'],
0x72: ['Hangul', 'Hangul'],
0x5C: ['Windows', 'Windows'],
}


def getKeyName(scanCode: int, isExtended: bool, shiftPressed: bool, capsLockOn: bool) -> str:
if scanCode in SCANCODE_NAMES:
code = SCANCODE_NAMES[scanCode]
else:
return f"Unknown scan code {hex(scanCode)}"

key = code[1] if shiftPressed else code[0]
return f'{key}' if len(key) > 1 else key
8 changes: 4 additions & 4 deletions pyrdp/mitm/BasePathMITM.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2019 GoSecure Inc.
# Copyright (C) 2019-2020 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

from pyrdp.mitm.state import RDPMITMState
from pyrdp.player import keyboard
from pyrdp.enum import ScanCode
from pyrdp.enum.scancode import getKeyName
from pyrdp.pdu.pdu import PDU
from pyrdp.layer.layer import Layer
from pyrdp.logging.StatCounter import StatCounter, STAT
Expand All @@ -32,7 +32,7 @@ def onScanCode(self, scanCode: int, isReleased: bool, isExtended: bool):
"""
Handle scan code.
"""
keyName = keyboard.getKeyName(scanCode, isExtended, self.state.shiftPressed, self.state.capsLockOn)
keyName = getKeyName(scanCode, isExtended, self.state.shiftPressed, self.state.capsLockOn)
scanCodeTuple = (scanCode, isExtended)

# Left or right shift
Expand Down Expand Up @@ -60,4 +60,4 @@ def onScanCode(self, scanCode: int, isReleased: bool, isExtended: bool):
# Normal input
elif len(keyName) == 1:
if not isReleased:
self.state.inputBuffer += keyName
self.state.inputBuffer += keyName
5 changes: 2 additions & 3 deletions pyrdp/mitm/FastPathMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
from pyrdp.logging.StatCounter import StatCounter, STAT
from pyrdp.mitm.state import RDPMITMState
from pyrdp.pdu import FastPathPDU, FastPathScanCodeEvent
from pyrdp.player import keyboard
from pyrdp.enum import ScanCode
from pyrdp.enum import scancode
from pyrdp.mitm.BasePathMITM import BasePathMITM

class FastPathMITM(BasePathMITM):
Expand Down Expand Up @@ -42,7 +41,7 @@ def onClientPDUReceived(self, pdu: FastPathPDU):
if not self.state.loggedIn:
for event in pdu.events:
if isinstance(event, FastPathScanCodeEvent):
self.onScanCode(event.scanCode, event.isReleased, event.rawHeaderByte & keyboard.KBDFLAGS_EXTENDED != 0)
self.onScanCode(event.scanCode, event.isReleased, event.rawHeaderByte & scancode.KBDFLAGS_EXTENDED != 0)

def onServerPDUReceived(self, pdu: FastPathPDU):
self.statCounter.increment(STAT.IO_OUTPUT_FASTPATH)
Expand Down
Loading

0 comments on commit 0c7f6ac

Please sign in to comment.