#!/usr/bin/python # # Copyright (c) 2014 Sylvain Peyrefitte # # This file is part of rdpy. # # rdpy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """ RDP proxy recorder and spy function Proxy RDP protocol """ import sys, os, getopt, json # Change path so we find rdpy sys.path.insert(1, os.path.join(sys.path[0], '..')) from rdpy.base import log, error from rdpy.protocol.rdp import rdp from rdpy.ui import view from twisted.internet import reactor from PyQt4 import QtCore, QtGui class ProxyServer(rdp.RDPServerObserver): """ Server side of proxy """ def __init__(self, controller, credentialProvider): """ @param controller: RDPServerController @param credentialProvider: CredentialProvider """ rdp.RDPServerObserver.__init__(self, controller) self._credentialProvider = credentialProvider self._client = None def clientConnected(self, client): """ Event throw by client when it's ready @param client: ProxyClient """ self._client = client self._controller.setColorDepth(self._client._controller.getColorDepth()) def onReady(self): """ Event use to inform state of server stack Use to connect client On ready is not launch only on connection but after a reactivation process """ if self._client is None: #try a connection domain, username, password = self._controller.getCredentials() try: ip, port = self._credentialProvider.getProxyPass(domain, username) except error.InvalidExpectedDataException as e: log.info(e.message) return width, height = self._controller.getScreen() reactor.connectTCP(ip, port, ProxyClientFactory(self, width, height, domain, username, password, "%s\\%s on %s:%s"%(domain, username, ip, port))) else: #refresh client width, height = self._controller.getScreen() self._client._controller.sendRefreshOrder(0, 0, width, height) def onClose(self): """ Call when client close connection """ if self._client is None: return self._client._controller.close() def onKeyEventScancode(self, code, isPressed): """ Event call when a keyboard event is catch in scan code format @param code: scan code of key @param isPressed: True if key is down """ #no client connected if self._client is None: return self._client._controller.sendKeyEventScancode(code, isPressed) def onKeyEventUnicode(self, code, isPressed): """ Event call when a keyboard event is catch in unicode format @param code: unicode of key @param isPressed: True if key is down """ #no client connected domain if self._client is None: return self._client._controller.sendKeyEventUnicode(code, isPressed) def onPointerEvent(self, x, y, button, isPressed): """ Event call on mouse event @param x: x position @param y: y position @param button: 1, 2 or 3 button @param isPressed: True if mouse button is pressed """ #no client connected if self._client is None: return self._client._controller.sendPointerEvent(x, y, button, isPressed) class ProxyServerFactory(rdp.ServerFactory): """ Factory on listening events """ def __init__(self, credentialProvider, privateKeyFilePath, certificateFilePath): """ @param config: rdp-proxy configuration @param credentialProvider: CredentialProvider """ rdp.ServerFactory.__init__(self, privateKeyFilePath, certificateFilePath, 16) self._credentialProvider = credentialProvider def buildObserver(self, controller): """ Implement rdp.ServerFactory @param controller: rdp.RDPServerController """ return ProxyServer(controller, self._credentialProvider) class ProxyClient(rdp.RDPClientObserver): """ Client side of proxy """ _CONNECTED_ = {} def __init__(self, controller, server, name): """ @param controller: RDPClientObserver @param server: ProxyServer @param name: name of session """ rdp.RDPClientObserver.__init__(self, controller) self._server = server self._name = name def onReady(self): """ Event use to signal that RDP stack is ready Inform proxy server that i'm connected implement RDPClientObserver """ ProxyClient._CONNECTED_[self._name] = self self._server.clientConnected(self) def onClose(self): """ Stack is closes """ if ProxyClient._CONNECTED_.has_key(self._name): del ProxyClient._CONNECTED_[self._name] self._server._controller.close() def onUpdate(self, destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, isCompress, data): """ Event use to inform bitmap update implement RDPClientObserver @param destLeft: xmin position @param destTop: ymin position @param destRight: xmax position because RDP can send bitmap with padding @param destBottom: ymax position because RDP can send bitmap with padding @param width: width of bitmap @param height: height of bitmap @param bitsPerPixel: number of bit per pixel @param isCompress: use RLE compression @param data: bitmap data """ self._server._controller.sendUpdate(destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, isCompress, data) class ProxyClientFactory(rdp.ClientFactory): """ Factory for proxy client """ def __init__(self, server, width, height, domain, username, password, name): """ @param server: ProxyServer @param width: screen width @param height: screen height @param domain: domain session @param username: username session @param password: password session @param name: name of session """ self._controller = server self._width = width self._height = height self._domain = domain self._username = username self._password = password self._name = name def buildObserver(self, controller): """ Implement rdp.ClientFactory Build observer (ProxyClient) @param controller: rdp.RDPClientController """ #set screen resolution controller.setScreen(self._width, self._height) #set credential controller.setDomain(self._domain) controller.setUsername(self._username) controller.setPassword(self._password) proxy = ProxyClient(controller, self._controller, self._name) return proxy def startedConnecting(self, connector): pass def clientConnectionLost(self, connector, reason): pass def clientConnectionFailed(self, connector, reason): pass class ProxyAdmin(rdp.RDPServerObserver): """ Use to manage client side of admin session Add GUI to select which session to see And manage see session Just escape key is authorized during spy session """ class State(object): """ GUI state -> list of active session SPY state -> watch active session """ GUI = 0 SPY = 1 def __init__(self, controller): """ @param server: rdp.RDPServerController """ rdp.RDPServerObserver.__init__(self, controller) self._controller = controller self._spy = None self._state = ProxyAdmin.State.GUI def initView(self): """ Init GUI view """ width, height = self._controller.getScreen() self._window = view.WindowView(width, height, QtGui.QColor(24, 93, 123)) self._list = view.ListView(ProxyClient._CONNECTED_.keys(), 500, 500, self.onSelect, QtGui.QColor(24, 93, 123)) self._window.addView(view.AnchorView(width / 2 - 250, height / 2 - 250, self._list)) self._render = view.RDPRenderer(self._controller, self._controller.getColorDepth()) def onReady(self): """ Stack is ready and connected May be called after an setColorDepth too """ if self._state == ProxyAdmin.State.GUI: self.initView() self._window.update(self._render, True) elif self._state == ProxyAdmin.State.SPY: #refresh client width, height = self._controller.getScreen() self._spy._controller.sendRefreshOrder(0, 0, width, height) def onClose(self): """ Stack is closes """ pass def onKeyEventScancode(self, code, isPressed): """ Event call when a keyboard event is catch in scan code format @param code: scan code of key @param isPressed: True if key is down """ if self._state == ProxyAdmin.State.GUI: if not isPressed: return self._window.keyEvent(code) self._window.update(self._render) elif code == 1: #escape button refresh GUI self._state = ProxyAdmin.State.GUI self._spy._controller.removeClientObserver(self._spy) self.onReady() def onKeyEventUnicode(self, code, isPressed): """ Event call when a keyboard event is catch in unicode format In admin mode is forbidden @param code: unicode of key @param isPressed: True if key is down """ pass def onPointerEvent(self, x, y, button, isPressed): """ Event call on mouse event In admin mode is forbidden @param x: x position @param y: y position @param button: 1, 2 or 3 button @param isPressed: True if mouse button is pressed """ pass def onSelect(self, name): """ Call back of list view @param name: name selected by user """ if not ProxyClient._CONNECTED_.has_key(name): return self._state = ProxyAdmin.State.SPY self._spy = ProxyClient(ProxyClient._CONNECTED_[name]._controller, self, "Admin") self._controller.setColorDepth(self._spy._controller.getColorDepth()) class ProxyAdminFactory(rdp.ServerFactory): """ Factory for admin """ def __init__(self, privateKeyFilePath, certificateFilePath): """ @param privateKeyFilePath: private key for admin session @param certificateFilePath: certificate for admin session """ rdp.ServerFactory.__init__(self, privateKeyFilePath, certificateFilePath, 16) def buildObserver(self, controller): """ Implement rdp.ServerFactory @param controller: rdp.RDPServerController """ return ProxyAdmin(controller) class CredentialProvider(object): """ Credential provider for proxy """ def __init__(self, config): """ @param config: rdp proxy config """ self._config = config def getAccount(self, domain, username): if not self._config.has_key(domain) or not self._config[domain].has_key(username): raise error.InvalidExpectedDataException("Invalid credentials %s\\%s"%(domain, username)) return self._config[domain][username] def getProxyPass(self, domain, username): """ @param domain: domain to check @param username: username in domain @return: (ip, port) or None if error """ account = self.getAccount(domain, username) if not account.has_key("ip") or not account.has_key("port"): raise error.InvalidExpectedDataException("Invalid credentials declaration %s\\%s"%(domain, username)) return str(account['ip']), account['port'] def help(): """ Print help in console """ print "Usage: rdpy-rdpproxy -f credential_file_path -k private_key_file_path -c certificate_file_path listen_port" def loadConfig(configFilePath): """ Load and check config file @param configFilePath: config file path """ if not os.path.isfile(configFilePath): log.error("File %s doesn't exist"%configFilePath) return None f = open(configFilePath, 'r') config = json.load(f) f.close() return config if __name__ == '__main__': configFilePath = None privateKeyFilePath = None certificateFilePath = None adminInterface = None try: opts, args = getopt.getopt(sys.argv[1:], "hf:k:c:i:") except getopt.GetoptError: help() for opt, arg in opts: if opt == "-h": help() sys.exit() elif opt == "-f": configFilePath = arg elif opt == "-k": privateKeyFilePath = arg elif opt == "-c": certificateFilePath = arg elif opt == "-i": adminInterface = arg if configFilePath is None: print "Config file is mandatory" help() sys.exit() if certificateFilePath is None: print "Certificate file is mandatory" help() sys.exit() if privateKeyFilePath is None: print "Private key file is mandatory" help() sys.exit() #load config file config = loadConfig(configFilePath) if config is None: log.error("Bad configuration file") sys.exit() #use to init font app = QtGui.QApplication(sys.argv) reactor.listenTCP(int(args[0]), ProxyServerFactory(CredentialProvider(config), privateKeyFilePath, certificateFilePath)) if not adminInterface is None: if ':' in adminInterface: adminInterface, adminPort = adminInterface.split(':') else: adminInterface, adminPort = adminInterface, "3390" log.info("Admin listen on %s:%s"%(adminInterface, adminPort)) reactor.listenTCP(int(adminPort), ProxyAdminFactory(privateKeyFilePath, certificateFilePath), interface = adminInterface) reactor.run()