1 - from aardwolf.extensions.RDPECLIP.protocol.formatlist import CLIPBRD_FORMAT 2 - from aardwolf.commons.queuedata.clipboard import RDP_CLIPBOARD_DATA_TXT 3 1 import sys 4 2 import asyncio 5 3 import traceback 6 4 import queue 7 5 import threading 6 + import time 8 7 9 8 from aardwolf import logger 9 + from aardwolf.keyboard import VK_MODIFIERS 10 10 from aardwolf.commons.url import RDPConnectionURL 11 11 from aardwolf.commons.iosettings import RDPIOSettings 12 12 from aardwolf.commons.queuedata import RDPDATATYPE 13 - from aardwolf.commons.queuedata.keyboard import RDP_KEYBOARD_SCANCODE 13 + from aardwolf.commons.queuedata.keyboard import RDP_KEYBOARD_SCANCODE, RDP_KEYBOARD_UNICODE 14 14 from aardwolf.commons.queuedata.mouse import RDP_MOUSE 15 + from aardwolf.extensions.RDPECLIP.protocol.formatlist import CLIPBRD_FORMAT 16 + from aardwolf.commons.queuedata.clipboard import RDP_CLIPBOARD_DATA_TXT 17 + from aardwolf.commons.queuedata.constants import MOUSEBUTTON, VIDEO_FORMAT 18 + from aardwolf.commons.target import RDPConnectionDialect 15 19 16 20 from PyQt5.QtWidgets import QApplication, QMainWindow, qApp, QLabel 17 21 from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QThread, Qt skipped 26 lines 44 48 45 49 def __init__(self, parent=None, **kwargs): 46 50 super().__init__(parent, **kwargs) 47 - self.settings = None 51 + self.settings: RDPClientConsoleSettings = None 48 52 self.conn = None 49 53 self.input_evt = None 50 54 self.in_q = None 51 55 self.loop_started_evt = threading.Event() 56 + self.gui_stopped_evt = threading.Event() 57 + self.input_handler_thread = None 58 + self.asyncthread:threading.Thread = None 52 59 53 60 def set_settings(self, settings, in_q): 54 61 self.settings = settings 55 62 self.in_q = in_q 56 63 57 - async def inputhandler(self): 58 - # This is super-ugly but I could not find a better solution 59 - # Problem is that pyqt5 is not async, and QT internally is using its own event loop 60 - # which makes everythign a mess. 61 - # If you know better (that doesn't require a noname unmaintained lib install) lemme know! 64 + def inputhandler(self, loop : asyncio . AbstractEventLoop ): 65 + while not self.conn.disconnected_evt.is_set(): 66 + data = self.in_q.get() 67 + loop.call_soon_threadsafe(self.conn.ext_in_queue.put_nowait, data) 68 + if data is None: 69 + break 70 + logger.debug('inputhandler terminating') 71 + 72 + async def ducky_keyboard_sender(self, scancode, is_pressed, as_char = False): 73 + ### Callback function for the duckyexecutor to dispatch scancodes/characters to the remote end 62 74 try: 63 - while not self.conn.disconnected_evt.is_set(): 64 - try: 65 - data = self.in_q.get(False) 66 - except queue.Empty: 67 - await asyncio.sleep(0.01) 68 - continue 69 - await self.conn.ext_in_queue.put(data) 70 - except: 75 + #print('SCANCODE: %s' % scancode) 76 + #print('is_pressed: %s' % is_pressed) 77 + #print('as_char: %s' % as_char) 78 + if as_char is False: 79 + ki = RDP_KEYBOARD_SCANCODE() 80 + ki.keyCode = scancode 81 + ki.is_pressed = is_pressed 82 + ki.modifiers = VK_MODIFIERS(0) 83 + await self.conn.ext_in_queue.put(ki) 84 + else: 85 + ki = RDP_KEYBOARD_UNICODE() 86 + ki.char = scancode 87 + ki.is_pressed = is_pressed 88 + await self.conn.ext_in_queue.put(ki) 89 + except Exception as e: 90 + traceback.print_exc() 91 + 92 + async def ducky_exec(self): 93 + try: 94 + from aardwolf.keyboard.layoutmanager import KeyboardLayoutManager 95 + from aardwolf.utils.ducky import DuckyExecutorBase, DuckyReaderFile 96 + if self.settings.iosettings.ducky_autostart_delay is not None: 97 + await asyncio.sleep(self.settings.iosettings.ducky_autostart_delay) 98 + 99 + layout = KeyboardLayoutManager().get_layout_by_shortname('enus') 100 + executor = DuckyExecutorBase(layout, self.ducky_keyboard_sender, send_as_char = True if self.conn.target.dialect == RDPConnectionDialect.VNC else False) 101 + reader = DuckyReaderFile.from_file(self.settings.iosettings.ducky_file, executor) 102 + await reader.parse() 103 + except Exception as e: 71 104 traceback.print_exc() 72 105 73 106 async def rdpconnection(self): skipped 4 lines 78 111 if err is not None: 79 112 raise err 80 113 81 - asyncio.create_task(self.inputhandler()) 114 + # asyncio.create_task(self.inputhandler()) 115 + input_handler_thread = asyncio.get_event_loop().run_in_executor(None, self.inputhandler, asyncio.get_event_loop()) 82 116 self.loop_started_evt.set() 83 - while True: 117 + if self.settings.iosettings.ducky_file is not None: 118 + x = asyncio.create_task(self.ducky_exec()) 119 + while not self.gui_stopped_evt.is_set(): 84 120 data = await self.conn.ext_out_queue.get() 85 121 if data is None: 86 122 return 87 123 if data.type == RDPDATATYPE.VIDEO: 88 - ri = RDPImage(data.x, data.y, data.data, data.height , data.width ) 89 - self.result.emit(ri) 124 + ri = RDPImage(data.x, data.y, data.data, data.width , data.height ) 125 + if not self.gui_stopped_evt.is_set(): 126 + self.result.emit(ri) 127 + else: 128 + return 90 129 elif data.type == RDPDATATYPE.CLIPBOARD_READY: 91 130 continue 131 + elif data.type == RDPDATATYPE.CLIPBOARD_NEW_DATA_AVAILABLE: 132 + continue 133 + elif data.type == RDPDATATYPE.CLIPBOARD_CONSUMED: 134 + continue 135 + elif data.type == RDPDATATYPE.CLIPBOARD_DATA_TXT: 136 + continue 92 137 else: 93 138 logger.debug('Unknown incoming data: %s'% data) 94 139 140 + except asyncio.CancelledError: 141 + return 95 142 96 143 except Exception as e: 97 144 traceback.print_exc() 98 145 finally: 99 - self.connection_terminated.emit() 146 + if self.conn is not None: 147 + await self.conn.terminate() 148 + input_handler_thread.cancel() 149 + if not self.gui_stopped_evt.is_set(): 150 + print(self.connection_terminated) 151 + self.connection_terminated.emit() 100 152 101 153 def starter(self): 102 154 self.loop = asyncio.new_event_loop() 103 155 asyncio.set_event_loop(self.loop) 104 - self.loop.run_until_complete(self.rdpconnection()) 105 - self.loop.close() 156 + try: 157 + self.rdp_connection_task = self.loop.create_task(self.rdpconnection()) 158 + self.loop.run_until_complete(self.rdp_connection_task) 159 + self.loop.close() 160 + except Exception as e: 161 + pass 162 + 106 163 107 164 @pyqtSlot() 108 165 def start(self): 109 166 # creating separate thread for async otherwise this will not return 110 167 # and then there will be no events sent back from application 111 - asyncthread = threading.Thread(target=self.starter, args=()) 112 - asyncthread.start() 168 + self . asyncthread = threading.Thread(target=self.starter, args=()) 169 + self . asyncthread.start() 170 + 171 + @pyqtSlot() 172 + def stop(self): 173 + self.gui_stopped_evt.set() 174 + if self.conn is not None: 175 + asyncio.run_coroutine_threadsafe(self.conn.terminate(), self.loop) 176 + time.sleep(0.1) # waiting connection to terminate 177 + self.rdp_connection_task.cancel() 178 + self.loop.stop() 113 179 114 180 115 181 class RDPClientQTGUI(QMainWindow): skipped 1 lines 117 183 118 184 def __init__(self, settings:RDPClientConsoleSettings): 119 185 super().__init__() 120 - # these sizes dont work :( 121 - #self.setMaximumSize(self.settings.iosettings.video_width, self.settings.iosettings.video_height) 122 - #self.setMinimumSize(self.settings.iosettings.video_width, self.settings.iosettings.video_height) 123 - #self.setFixedSize(self.settings.iosettings.video_width, self.settings.iosettings.video_height) 124 186 self.settings = settings 125 187 126 188 # enabling this will singificantly increase the bandwith 127 189 self.mhover = settings.mhover 128 190 # enabling keyboard tracking 129 191 self.keyboard = settings.keyboard 192 + self.is_rdp = True if settings.url.lower().startswith('rdp') is True else False 130 193 131 194 # setting up the main window with the requested resolution 132 - self.setGeometry(0,0, self.settings.iosettings.video_width, self.settings.iosettings.video_height) 195 + self.setGeometry(0, 0, self.settings.iosettings.video_width, self.settings.iosettings.video_height) 133 196 # this buffer will hold the current frame and will be contantly updated 134 197 # as new rectangle info comes in from the server 135 198 self._buffer = QImage(self.settings.iosettings.video_width, self.settings.iosettings.video_height, QImage.Format_RGB32) skipped 23 lines 159 222 # enabling mouse tracking 160 223 self.setMouseTracking(True) 161 224 self._label_imageDisplay.setMouseTracking(True) 225 + self.__extended_rdp_keys = { 226 + Qt.Key_End : 'VK_END', 227 + Qt.Key_Down : 'VK_DOWN', 228 + Qt.Key_PageDown : 'VK_NEXT', 229 + Qt.Key_Insert : 'VK_INSERT', 230 + Qt.Key_Delete : 'VK_DELETE', 231 + Qt.Key_Print : 'VK_SNAPSHOT', 232 + Qt.Key_Home : 'VK_HOME', 233 + Qt.Key_Up : 'VK_UP', 234 + Qt.Key_PageUp : 'VK_PRIOR', 235 + Qt.Key_Left : 'VK_LEFT', 236 + Qt.Key_Right : 'VK_RIGHT', 237 + Qt.Key_Meta : 'VK_LWIN', 238 + Qt.Key_Enter : 'VK_RETURN', 239 + Qt.Key_Menu : 'VK_LMENU', 240 + Qt.Key_Pause : 'VK_PAUSE', 241 + Qt.Key_Slash: 'VK_DIVIDE', 242 + Qt.Key_Period: 'VK_DECIMAL', 243 + 244 + #Qt.Key_Shift: 'VK_LSHIFT', 245 + #Qt.Key_Tab: 'VK_TAB', 246 + #Qt.Key_0 : 'VK_NUMPAD0', 247 + #Qt.Key_1 : 'VK_NUMPAD1', 248 + #Qt.Key_2 : 'VK_NUMPAD2', 249 + #Qt.Key_3 : 'VK_NUMPAD3', 250 + #Qt.Key_4 : 'VK_NUMPAD4', 251 + #Qt.Key_5 : 'VK_NUMPAD5', 252 + #Qt.Key_6 : 'VK_NUMPAD6', 253 + #Qt.Key_7 : 'VK_NUMPAD7', 254 + #Qt.Key_8 : 'VK_NUMPAD8', 255 + #Qt.Key_9 : 'VK_NUMPAD9', 256 + } 257 + 258 + self.__qtbutton_to_rdp = { 259 + Qt.LeftButton : MOUSEBUTTON.MOUSEBUTTON_LEFT, 260 + Qt.RightButton : MOUSEBUTTON.MOUSEBUTTON_RIGHT, 261 + Qt.MidButton : MOUSEBUTTON.MOUSEBUTTON_MIDDLE, 262 + Qt.ExtraButton1 : MOUSEBUTTON.MOUSEBUTTON_5, 263 + Qt.ExtraButton2 : MOUSEBUTTON.MOUSEBUTTON_6, 264 + Qt.ExtraButton3 : MOUSEBUTTON.MOUSEBUTTON_7, 265 + Qt.ExtraButton4 : MOUSEBUTTON.MOUSEBUTTON_8, 266 + Qt.ExtraButton5 : MOUSEBUTTON.MOUSEBUTTON_9, 267 + Qt.ExtraButton6 : MOUSEBUTTON.MOUSEBUTTON_10, 268 + } 162 269 163 270 def closeEvent(self, event): 164 - self._thread.quit() 271 + self.connectionClosed() 165 272 event.accept() 166 273 167 274 def connectionClosed(self): 168 - print('its over!') 275 + self.in_q.put(None) 276 + self._threaded.stop() 277 + self._thread.quit() 169 278 170 279 def updateImage(self, event): 171 - with QPainter(self._buffer) as qp: 172 - qp.drawImage(event.x, event.y, event.image, 0, 0, event.width, event.height) 280 + if event.width == self.settings.iosettings.video_width and event.height == self.settings.iosettings.video_height: 281 + self._buffer = event.image 282 + else: 283 + with QPainter(self._buffer) as qp: 284 + qp.drawImage(event.x, event.y, event.image, 0, 0, event.width, event.height) 285 + 173 286 pixmap01 = QPixmap.fromImage(self._buffer) 174 287 pixmap_image = QPixmap(pixmap01) 175 288 self._label_imageDisplay.setPixmap(pixmap_image) skipped 2 lines 178 291 self._label_imageDisplay.setMinimumSize(1,1) 179 292 self._label_imageDisplay.show() 180 293 181 - def keyPressEvent(self, e): 294 + ## this is for testing! 295 + #def keyevent_to_string(self, event): 296 + # keymap = {} 297 + # for key, value in vars(Qt).items(): 298 + # if isinstance(value, Qt.Key): 299 + # keymap[value] = key.partition('_')[2] 300 + # modmap = { 301 + # Qt.ControlModifier: keymap[Qt.Key_Control], 302 + # Qt.AltModifier: keymap[Qt.Key_Alt], 303 + # Qt.ShiftModifier: keymap[Qt.Key_Shift], 304 + # Qt.MetaModifier: keymap[Qt.Key_Meta], 305 + # Qt.GroupSwitchModifier: keymap[Qt.Key_AltGr], 306 + # Qt.KeypadModifier: keymap[Qt.Key_NumLock], 307 + # } 308 + # sequence = [] 309 + # for modifier, text in modmap.items(): 310 + # if event.modifiers() & modifier: 311 + # sequence.append(text) 312 + # key = keymap.get(event.key(), event.text()) 313 + # if key not in sequence: 314 + # sequence.append(key) 315 + # return '+'.join(sequence) 316 + 317 + def send_key(self, e, is_pressed): 318 + # https://doc.qt.io/qt-5/qt.html#Key-enum 182 319 if self.keyboard is False: 183 320 return 321 + #print(self.keyevent_to_string(e)) 184 322 185 323 if e.key()==(Qt.Key_Control and Qt.Key_V): 186 324 ki = RDP_CLIPBOARD_DATA_TXT() skipped 1 lines 188 326 ki.data = pyperclip.paste() 189 327 self.in_q.put(ki) 190 328 191 - #print('SCANCODE: %s' % e.nativeScanCode()) 329 + modifiers = VK_MODIFIERS(0) 330 + qt_modifiers = QApplication.keyboardModifiers() 331 + if bool(qt_modifiers & Qt.ShiftModifier) is True and e.key() != Qt.Key_Shift: 332 + modifiers |= VK_MODIFIERS.VK_SHIFT 333 + if bool(qt_modifiers & Qt.ControlModifier) is True and e.key() != Qt.Key_Control: 334 + modifiers |= VK_MODIFIERS.VK_CONTROL 335 + if bool(qt_modifiers & Qt.AltModifier) is True and e.key() != Qt.Key_Alt: 336 + modifiers |= VK_MODIFIERS.VK_MENU 337 + if bool(qt_modifiers & Qt.KeypadModifier) is True and e.key() != Qt.Key_NumLock: 338 + modifiers |= VK_MODIFIERS.VK_NUMLOCK 339 + if bool(qt_modifiers & Qt.MetaModifier) is True and e.key() != Qt.Key_Meta: 340 + modifiers |= VK_MODIFIERS.VK_WIN 341 + 192 342 ki = RDP_KEYBOARD_SCANCODE() 193 343 ki.keyCode = e.nativeScanCode() 194 - ki.is_pressed = True 344 + ki.is_pressed = is_pressed 195 345 if sys.platform == "linux": 196 346 #why tho? 197 347 ki.keyCode -= 8 198 - self.in_q.put(ki) 348 + ki.modifiers = modifiers 199 349 200 - def keyReleaseEvent(self, e): 201 - if self.keyboard is False: 202 - return 203 - ki = RDP_KEYBOARD_SCANCODE() 204 - ki.keyCode = e.nativeScanCode() 205 - ki.is_pressed = False 206 - if sys.platform == "linux": 207 - ki.keyCode -= 8 350 + if e.key() in self.__extended_rdp_keys.keys(): 351 + ki.vk_code = self.__extended_rdp_keys[e.key()] 352 + 353 + #print('SCANCODE: %s' % ki.keyCode) 354 + #print('VK CODE : %s' % ki.vk_code) 355 + #print('TEXT : %s' % repr(e.text())) 208 356 self.in_q.put(ki) 209 357 210 - def mouseMoveEvent(self, e): 211 - if self.mhover is False: 358 + def send_mouse(self, e, is_pressed, is_hover = False): 359 + if is_hover is True and self. settings .mhover is False: 360 + # is hovering is disabled we return immediately 212 361 return 213 - mi = RDP_MOUSE() 214 - mi.xPos = e.pos().x() 215 - mi.yPos = e.pos().y() 216 - mi.button = 0 217 - mi.pressed = False 218 - self.in_q.put(mi) 219 - 220 - def mouseReleaseEvent(self, e): 221 - """ 222 - mouse button release event 223 - """ 224 - button = e.button() 225 - buttonNumber = 0 226 - if button == Qt.LeftButton: 227 - buttonNumber = 1 228 - elif button == Qt.RightButton: 229 - buttonNumber = 2 230 - elif button == Qt.MidButton: 231 - buttonNumber = 3 362 + buttonNumber = MOUSEBUTTON.MOUSEBUTTON_HOVER 363 + if is_hover is False: 364 + buttonNumber = self.__qtbutton_to_rdp[e.button()] 232 365 233 366 mi = RDP_MOUSE() 234 367 mi.xPos = e.pos().x() 235 368 mi.yPos = e.pos().y() 236 369 mi.button = buttonNumber 237 - mi.pressed = False 370 + mi.is_pressed = is_pressed if is_hover is False else False 238 371 239 372 self.in_q.put(mi) 240 373 374 + def keyPressEvent(self, e): 375 + self.send_key(e, True) 241 376 377 + def keyReleaseEvent(self, e): 378 + self.send_key(e, False) 242 379 243 - def mousePressEvent(self, e): 244 - button = e.button() 245 - buttonNumber = 0 246 - if button == Qt.LeftButton: 247 - buttonNumber = 1 248 - elif button == Qt.RightButton: 249 - buttonNumber = 2 250 - elif button == Qt.MidButton: 251 - buttonNumber = 3 380 + def mouseMoveEvent(self, e): 381 + self.send_mouse(e, False, True) 252 382 253 - mi = RDP_MOUSE() 254 - mi.xPos = e.pos().x() 255 - mi.yPos = e.pos().y() 256 - mi.button = buttonNumber 257 - mi.pressed = True 383 + def mouseReleaseEvent(self, e): 384 + self.send_mouse(e, False) 258 385 259 - self.in_q.put(mi) 386 + def mousePressEvent(self, e): 387 + self.send_mouse(e, True) 260 388 261 389 262 390 def main(): skipped 5 lines 268 396 parser.add_argument('--no-keyboard', action='store_false', help='Disables keyboard input. (whatever)') 269 397 parser.add_argument('--res', default = '1024x768', help='Resolution in "WIDTHxHEIGHT" format. Default: "1024x768"') 270 398 parser.add_argument('--bpp', choices = [15, 16, 24, 32], default = 32, type=int, help='Bits per pixel.') 399 + parser.add_argument('--ducky', help='Ducky script to be executed') 271 400 parser.add_argument('url', help="RDP connection url") 272 401 273 402 args = parser.parse_args() skipped 8 lines 282 411 width, height = args.res.upper().split('X') 283 412 height = int(height) 284 413 width = int(width) 285 - 286 414 iosettings = RDPIOSettings() 287 415 iosettings.video_width = width 288 416 iosettings.video_height = height 289 417 iosettings.video_bpp_min = 15 #servers dont support 8 any more :/ 290 418 iosettings.video_bpp_max = args.bpp 291 - iosettings.video_out_format = ' qt ' 419 + iosettings.video_out_format = VIDEO_FORMAT . QT5 420 + iosettings.ducky_file = args.ducky 421 + iosettings.ducky_autostart_delay = 5 292 422 293 423 settings = RDPClientConsoleSettings(args.url, iosettings) 294 424 settings.mhover = args.no_mouse_hover skipped 11 lines