| skipped 6 lines |
7 | 7 | | # TODO: Add mouse scroll functionality (QT5 client needs to be modified for that) |
8 | 8 | | |
9 | 9 | | import io |
| 10 | + | import copy |
10 | 11 | | import asyncio |
11 | 12 | | import traceback |
12 | 13 | | from struct import pack, unpack |
| skipped 11 lines |
24 | 25 | | from aardwolf.keyboard import VK_MODIFIERS |
25 | 26 | | from aardwolf.keyboard.layoutmanager import KeyboardLayoutManager |
26 | 27 | | from aardwolf.commons.queuedata.constants import MOUSEBUTTON, VIDEO_FORMAT |
| 28 | + | from aardwolf.commons.iosettings import RDPIOSettings |
27 | 29 | | |
28 | 30 | | from PIL import Image |
29 | | - | from PIL.ImageQt import ImageQt |
| 31 | + | try: |
| 32 | + | from PIL.ImageQt import ImageQt |
| 33 | + | except ImportError: |
| 34 | + | print('No Qt installed! Converting to qt will not work') |
| 35 | + | |
30 | 36 | | import rle |
31 | 37 | | |
32 | 38 | | # https://datatracker.ietf.org/doc/html/rfc6143 |
| skipped 10 lines |
43 | 49 | | ZRLE_ENCODING = 16 |
44 | 50 | | |
45 | 51 | | class VNCConnection: |
46 | | - | def __init__(self, target, credentials, iosettings): |
| 52 | + | def __init__(self, target, credentials, iosettings:RDPIOSettings): |
47 | 53 | | self.target = target |
48 | 54 | | self.credentials = credentials |
49 | 55 | | self.authapi = None |
| skipped 34 lines |
84 | 90 | | self.width = None |
85 | 91 | | self.height = None |
86 | 92 | | self.__desktop_buffer = None |
| 93 | + | self.desktop_buffer_has_data = False |
87 | 94 | | |
88 | 95 | | self.__vk_to_vnckey = { |
89 | 96 | | 'VK_BACK' : KEY_BackSpace, |
| skipped 136 lines |
226 | 233 | | raise err |
227 | 234 | | logger.debug('Client init OK') |
228 | 235 | | |
| 236 | + | logger.debug('Starting internal comm channels') |
229 | 237 | | self.__reader_loop_task = asyncio.create_task(self.__reader_loop()) |
230 | 238 | | self.__external_reader_task = asyncio.create_task(self.__external_reader()) |
| 239 | + | logger.debug('Sending cliboard ready signal') #to emulate RDP clipboard functionality |
| 240 | + | msg = RDP_CLIPBOARD_READY() |
| 241 | + | await self.ext_out_queue.put(msg) |
231 | 242 | | |
232 | 243 | | return True, None |
233 | 244 | | except Exception as e: |
| skipped 40 lines |
274 | 285 | | for sectype in sec_types: |
275 | 286 | | self.server_supp_security_types.append(sectype) |
276 | 287 | | |
277 | | - | if self.__selected_security_type == 2: |
| 288 | + | if self.__selected_security_type == 0: |
| 289 | + | logger.debug('Invalid authentication type!!!') |
| 290 | + | raise Exception('Invalid authentication type') |
| 291 | + | |
| 292 | + | elif self.__selected_security_type == 1: |
| 293 | + | # nothing to do here |
| 294 | + | logger.debug('Selecting NULL auth type') |
| 295 | + | |
| 296 | + | |
| 297 | + | elif self.__selected_security_type == 2: |
278 | 298 | | logger.debug('Selecting default VNC auth type') |
279 | 299 | | self.__writer.write(bytes([self.__selected_security_type])) |
280 | 300 | | challenge = await self.__reader.readexactly(16) |
| skipped 70 lines |
351 | 371 | | except Exception as e: |
352 | 372 | | return None, e |
353 | 373 | | |
| 374 | + | async def send_key_virtualkey(self, vk:str, is_pressed:bool, is_extended:bool, scancode_hint:int = None): |
| 375 | + | try: |
| 376 | + | if indata.vk_code is not None: |
| 377 | + | vk_code = indata.vk_code |
| 378 | + | else: |
| 379 | + | vk_code = self.__keyboard_layout.scancode_to_vk(indata.keyCode) |
| 380 | + | print('Got VK: %s' % vk_code) |
| 381 | + | if vk_code is None: |
| 382 | + | print('Could not map SC to VK! SC: %s' % indata.keyCode) |
| 383 | + | if vk_code is not None and vk_code in self.__vk_to_vnckey: |
| 384 | + | keycode = self.__vk_to_vnckey[vk_code] |
| 385 | + | print('AAAAAAAA %s' % hex(keycode)) |
| 386 | + | |
| 387 | + | |
| 388 | + | if vk in self.__vk_to_sc: |
| 389 | + | scancode = self.__vk_to_sc[vk] |
| 390 | + | is_extended = True |
| 391 | + | print('EXT') |
| 392 | + | else: |
| 393 | + | scancode = scancode_hint |
| 394 | + | return await self.send_key_char(scancode, is_pressed, is_extended) |
| 395 | + | except Exception as e: |
| 396 | + | traceback.print_exc() |
| 397 | + | return None, e |
| 398 | + | |
| 399 | + | async def send_key_scancode(self, scancode, is_pressed, is_extended): |
| 400 | + | try: |
| 401 | + | keycode = self.__keyboard_layout.scancode_to_char(indata.keyCode, modifiers) |
| 402 | + | print(keycode) |
| 403 | + | if keycode is None: |
| 404 | + | print('Failed to resolv key! SC: %s VK: %s' % (indata.keyCode, vk_code)) |
| 405 | + | #continue |
| 406 | + | elif keycode is not None and len(keycode) == 1: |
| 407 | + | keycode = ord(keycode) |
| 408 | + | print('Keycode %s resolved to: %s' % (indata.keyCode , repr(keycode))) |
| 409 | + | elif keycode is not None and len(keycode) > 1: |
| 410 | + | print('LARGE! Keycode %s resolved to: %s' % (indata.keyCode , repr(keycode))) |
| 411 | + | #continue |
| 412 | + | else: |
| 413 | + | print('This key is too special! Can\'t resolve it! SC: %s VK: %s' % (indata.keyCode, vk_code)) |
| 414 | + | #continue |
| 415 | + | |
| 416 | + | return True, None |
| 417 | + | except Exception as e: |
| 418 | + | traceback.print_exc() |
| 419 | + | return None, e |
| 420 | + | |
| 421 | + | async def send_key_char(self, char, is_pressed): |
| 422 | + | try: |
| 423 | + | msg = pack("!BBxxI", 4, int(is_pressed), char) |
| 424 | + | self.__writer.write(msg) |
| 425 | + | return True, None |
| 426 | + | except Exception as e: |
| 427 | + | traceback.print_exc() |
| 428 | + | return None, e |
| 429 | + | |
| 430 | + | async def send_mouse(self, button:MOUSEBUTTON, xPos:int, yPos:int, is_pressed:bool): |
| 431 | + | try: |
| 432 | + | if xPos < 0 or yPos < 0: |
| 433 | + | return True, None |
| 434 | + | |
| 435 | + | button =0 |
| 436 | + | if button == MOUSEBUTTON.MOUSEBUTTON_LEFT: |
| 437 | + | button = 1 |
| 438 | + | elif button == MOUSEBUTTON.MOUSEBUTTON_MIDDLE: |
| 439 | + | button = 2 |
| 440 | + | elif button == MOUSEBUTTON.MOUSEBUTTON_RIGHT: |
| 441 | + | button = 3 |
| 442 | + | |
| 443 | + | buttonmask = 0 |
| 444 | + | if is_pressed is True: |
| 445 | + | if button == 1: buttonmask &= ~1 |
| 446 | + | if button == 2: buttonmask &= ~2 |
| 447 | + | if button == 3: buttonmask &= ~4 |
| 448 | + | if button == 4: buttonmask &= ~8 |
| 449 | + | if button == 5: buttonmask &= ~16 |
| 450 | + | else: |
| 451 | + | if button == 1: buttonmask |= 1 |
| 452 | + | if button == 2: buttonmask |= 2 |
| 453 | + | if button == 3: buttonmask |= 4 |
| 454 | + | if button == 4: buttonmask |= 8 |
| 455 | + | if button == 5: buttonmask |= 16 |
| 456 | + | |
| 457 | + | msg = pack("!BBHH", 5, buttonmask, xPos, yPos) |
| 458 | + | self.__writer.write(msg) |
| 459 | + | return True, None |
| 460 | + | except Exception as e: |
| 461 | + | traceback.print_exc() |
| 462 | + | return None, e |
| 463 | + | |
| 464 | + | def get_desktop_buffer(self, encoding:VIDEO_FORMAT = VIDEO_FORMAT.PIL): |
| 465 | + | """Makes a copy of the current desktop buffer, converts it and returns the object""" |
| 466 | + | try: |
| 467 | + | image = self.__desktop_buffer.copy() |
| 468 | + | if encoding == VIDEO_FORMAT.PIL: |
| 469 | + | return image |
| 470 | + | elif encoding == VIDEO_FORMAT.RAW: |
| 471 | + | return image.tobytes() |
| 472 | + | elif encoding == VIDEO_FORMAT.QT5: |
| 473 | + | return ImageQt(image) |
| 474 | + | elif encoding == VIDEO_FORMAT.PNG: |
| 475 | + | img_byte_arr = io.BytesIO() |
| 476 | + | image.save(img_byte_arr, format='PNG') |
| 477 | + | return img_byte_arr.getvalue() |
| 478 | + | else: |
| 479 | + | raise ValueError('Output format of "%s" is not supported!' % encoding) |
| 480 | + | except Exception as e: |
| 481 | + | traceback.print_exc() |
| 482 | + | return None, e |
| 483 | + | |
354 | 484 | | async def __external_reader(self): |
355 | 485 | | # This coroutine handles keyboard/mouse/clipboard etc input from the user |
356 | 486 | | # It wraps the data in it's appropriate format then dispatches it to the server |
| skipped 72 lines |
429 | 559 | | else: |
430 | 560 | | # I hope you know what you're doing here... |
431 | 561 | | keycode = int.from_bytes(bytes.fromhex(keycode), byteorder = 'big', signed = False) |
432 | | - | msg = pack("!BBxxI", 4, int(indata.is_pressed), indata.char) |
433 | | - | self.__writer.write(msg) |
| 562 | + | await self.send_key_char(keycode, indata.is_pressed) |
434 | 563 | | |
435 | 564 | | elif indata.type == RDPDATATYPE.MOUSE: |
436 | 565 | | #PointerEvent |
| skipped 1 lines |
438 | 567 | | if indata.xPos < 0 or indata.yPos < 0: |
439 | 568 | | continue |
440 | 569 | | |
441 | | - | button =0 |
442 | | - | if indata.button == MOUSEBUTTON.MOUSEBUTTON_LEFT: |
443 | | - | button = 1 |
444 | | - | elif indata.button == MOUSEBUTTON.MOUSEBUTTON_MIDDLE: |
445 | | - | button = 2 |
446 | | - | elif indata.button == MOUSEBUTTON.MOUSEBUTTON_RIGHT: |
447 | | - | button = 3 |
448 | | - | |
449 | | - | buttonmask = 0 |
450 | | - | if indata.is_pressed is True: |
451 | | - | if button == 1: buttonmask &= ~1 |
452 | | - | if button == 2: buttonmask &= ~2 |
453 | | - | if button == 3: buttonmask &= ~4 |
454 | | - | if button == 4: buttonmask &= ~8 |
455 | | - | if button == 5: buttonmask &= ~16 |
456 | | - | else: |
457 | | - | if button == 1: buttonmask |= 1 |
458 | | - | if button == 2: buttonmask |= 2 |
459 | | - | if button == 3: buttonmask |= 4 |
460 | | - | if button == 4: buttonmask |= 8 |
461 | | - | if button == 5: buttonmask |= 16 |
462 | | - | |
463 | | - | |
464 | | - | #print('sending mouse!') |
465 | | - | msg = pack("!BBHH", 5, buttonmask, indata.xPos, indata.yPos) |
466 | | - | self.__writer.write(msg) |
| 570 | + | await self.send_mouse(indata.button, indata.xPos, indata.yPos, indata.is_pressed) |
467 | 571 | | |
468 | 572 | | elif indata.type == RDPDATATYPE.CLIPBOARD_DATA_TXT: |
469 | 573 | | try: |
| skipped 30 lines |
500 | 604 | | async def __send_rect(self, x, y, width, height, image:Image): |
501 | 605 | | try: |
502 | 606 | | #updating desktop buffer to have a way to copy rectangles later |
| 607 | + | self.desktop_buffer_has_data = True |
503 | 608 | | if self.width == width and self.height == height: |
504 | 609 | | self.__desktop_buffer = image |
505 | 610 | | else: |
| skipped 22 lines |
528 | 633 | | rect.data = image |
529 | 634 | | await self.ext_out_queue.put(rect) |
530 | 635 | | |
531 | | - | |
532 | 636 | | except Exception as e: |
533 | 637 | | await self.terminate() |
534 | 638 | | return None, e |
535 | | - | |
536 | | - | |
537 | 639 | | |
538 | 640 | | async def __reader_loop(self): |
539 | 641 | | try: |
| skipped 97 lines |
637 | 739 | | |
638 | 740 | | elif msgtype == 3: |
639 | 741 | | # Server side has updated the clipboard |
| 742 | + | |
| 743 | + | # Signaling clipboard data (to simulate RDP functionality) |
| 744 | + | msg = RDP_CLIPBOARD_NEW_DATA_AVAILABLE() |
| 745 | + | await self.ext_out_queue.put(msg) |
| 746 | + | |
| 747 | + | # processing clipboard data |
640 | 748 | | hdr = await self.__reader.readexactly(7) |
641 | 749 | | (_,_,_, cliplen ) = unpack("!BBBI", hdr) |
642 | 750 | | cliptext = await self.__reader.readexactly(cliplen) |
643 | | - | cliptext = cliptext.decode('latin-1') |
| 751 | + | cliptext = cliptext.decode('latin-1') #latin-1 is per RFC |
644 | 752 | | logger.info('Got clipboard test: %s' % repr(cliptext)) |
645 | 753 | | if self.__use_pyperclip is True: |
646 | 754 | | import pyperclip |
| skipped 80 lines |