import asyncio from PIL import Image import io import numpy as np import zlib # This results in an RGBA organization of pixels MYPIXFORMAT = bytearray([ 32, # bits per pixel 24, # depth 0, # big endian 1, # true color 0, 255, # red max 0, 255, # green max 0, 255, # blue max 0, 8, 16, # red shift, green shift, blue shift 0, 0, 0 # padding ]) class ByteStream: def __init__(self): self.buffer = b'' def add_number(self, number, num_bytes): data = number.to_bytes(num_bytes, byteorder='big', signed=True) self.buffer += data def extend(self, data): self.buffer += data def get_bytes(self): return self.buffer def clear(self): self.buffer = b'' def flush(self, writer): writer.write(self.buffer) self.clear() class VNCClient: async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): await self.close() return False @classmethod async def create(cls, url): self = cls() if url.startswith('unix://'): url = url.replace('unix://', '') if url.startswith('/'): self.reader, self.writer = await asyncio.open_unix_connection(url) elif url.startswith('@'): url = '\0' + url[1:] self.reader, self.writer = await asyncio.open_unix_connection(url) elif url.startswith('tcp://'): url = url.replace('tcp://', '') host, port = url.split(':') self.reader, self.writer = await asyncio.open_connection(host, int(port)) else: raise ValueError('Unsupported URL: {}'.format(url)) self.receiver = None self.framebuffer = None self.copytext = None self._updating = True self.decompressor = zlib.decompressobj() self._input_queue = asyncio.Queue() self._input_task = asyncio.create_task(self._input_worker()) await self._do_vnc_handshake() return self async def _input_worker(self): while True: keys, modifierkeys = await self._input_queue.get() payload = ByteStream() for modkey in (modifierkeys or []): payload.add_number(4, 1) # Key event payload.add_number(1, 1) # Down payload.add_number(0, 2) # Padding payload.add_number(modkey.value, 4) payload.flush(self.writer) await self.writer.drain() for key in keys: keynumber = key.value if hasattr(key, 'value') else key payload.add_number(4, 1) # Key event payload.add_number(1, 1) # Down payload.add_number(0, 2) # Padding payload.add_number(keynumber, 4) payload.add_number(4, 1) # Key event payload.add_number(0, 1) # Up payload.add_number(0, 2) # Padding payload.add_number(keynumber, 4) payload.flush(self.writer) await self.writer.drain() for modkey in (modifierkeys or []): payload.add_number(4, 1) # Key event payload.add_number(0, 1) # Up payload.add_number(0, 2) # Padding payload.add_number(modkey.value, 4) payload.flush(self.writer) await self.writer.drain() await asyncio.sleep(0.01) # Have to slow down keypresses for some servers # Still shouldn't be noticable interactively, but does slow down paste to a fast typist... self._input_queue.task_done() async def send_keypresses(self, keys, modifierkeys=None): await self._input_queue.put((keys, modifierkeys)) async def _read_number(self, num_bytes): data = await self.reader.readexactly(num_bytes) return int.from_bytes(data, byteorder='big', signed=True) def _write_number(self, number, num_bytes): data = number.to_bytes(num_bytes, byteorder='big', signed=True) self.writer.write(data) return data async def get_screenshot(self): while self._updating: await asyncio.sleep(0.1) await asyncio.sleep(0) if self.framebuffer is None: raise Exception('No framebuffer data available') self._updating = True return self.framebuffer.copy() async def _do_vnc_handshake(self): rfbver = await self.reader.readline() if not rfbver.startswith(b'RFB 003.008'): self.writer.close() await self.writer.wait_closed() raise Exception('Unsupported RFB version') self.writer.write(b'RFB 003.008\n') numsectypes = await self._read_number(1) if not numsectypes: self.writer.close() await self.writer.wait_closed() raise Exception('No security types supported by the server') sectypes = await self.reader.readexactly(numsectypes) sectypes = bytearray(sectypes) secresult = 1 if 1 in sectypes: self.writer.write(b'\x01') await self.writer.drain() secresult = await self._read_number(4) # Security result if secresult != 0: self.writer.close() await self.writer.wait_closed() raise Exception('VNC authentication failed') self.writer.write(b'\x01') # Share display self.width = await self._read_number(2) self.height = await self._read_number(2) pixformat = await self.reader.readexactly(16) name_length = await self._read_number(4) self.name = await self.reader.readexactly(name_length) payload = ByteStream() if pixformat != MYPIXFORMAT: payload.add_number(0, 1) # Set pixel format payload.add_number(0, 3) # Padding payload.extend(MYPIXFORMAT) payload.flush(self.writer) self.receiver = asyncio.create_task(self._receive_loop()) payload.add_number(2, 1) # Set encodings payload.add_number(0, 1) # Padding payload.add_number(4, 2) # Number of encodings payload.add_number(6, 4) # zlib payload.add_number(7, 4) # tight payload.add_number(-223, 4) # desktopsize payload.add_number(-308, 4) # extended desktopsize payload.flush(self.writer) self._request_screen_update(incremental=False) def _request_screen_update(self, incremental=True): incremental = 1 if incremental else 0 payload = ByteStream() payload.add_number(3, 1) # Framebuffer update request payload.add_number(incremental, 1) # Incremental payload.add_number(0, 2) # x position payload.add_number(0, 2) # y position payload.add_number(self.width, 2) # width payload.add_number(self.height, 2) # height payload.flush(self.writer) async def _receive_loop(self): while True: try: message_type = await self._read_number(1) if message_type == 0: # Framebuffer update await self._handle_framebuffer_update() elif message_type == 1: # Set color map entries raise NotImplementedError('Set color map entries not implemented') elif message_type == 2: # Bell pass elif message_type == 3: # Server cut text padding = await self._read_number(3) length = await self._read_number(4) self.copytext = await self.reader.readexactly(length) else: raise Exception(f'Unknown message type: {message_type}') except Exception as e: print(f"Error in receive loop: {e}") break async def _handle_framebuffer_update(self): _ = await self._read_number(1) # Padding num_rects = await self._read_number(2) self._updating = True for _ in range(num_rects): await self._handle_rectangle() self._updating = False self._request_screen_update(incremental=True) async def _handle_rectangle(self): if self.framebuffer is None: self.framebuffer = Image.new('RGBA', (self.width, self.height)) x = await self._read_number(2) y = await self._read_number(2) width = await self._read_number(2) height = await self._read_number(2) encoding_type = await self._read_number(4) pixel_data = None if encoding_type == 6: compressed_data_length = await self._read_number(4) compressed_data = await self.reader.readexactly(compressed_data_length) # Decompress the data using zlib and store it in the framebuffer pixel_data = self.decompressor.decompress(compressed_data) elif encoding_type == 0: pixel_data = await self.reader.readexactly(width * height * 4) # Assuming 32 bits per pixel if encoding_type in (-223, -308): # desktopsize self.width = width self.height = height self.framebuffer = Image.new('RGBA', (self.width, self.height)) if encoding_type == -308: nscreens = await self._read_number(1) _ = await self._read_number(3) # padding for _ in range(nscreens): _ = await self.reader.readexactly(16) # screen info elif pixel_data: pixel_data = np.frombuffer(pixel_data, dtype=np.uint8).reshape((height, width, 4)).copy() pixel_data[:, :, 3] = 0xff img = Image.fromarray(pixel_data, 'RGBA') self.framebuffer.paste(img, (x, y)) elif encoding_type == 7: # tight # Best document I could see was: # https://github.com/TurboVNC/tightvnc/blob/main/vnc_winsrc/rfb/rfbproto.h tightheader = await self._read_number(1) streamid = tightheader & 0x0F if streamid: raise NotImplementedError('tight encoding with streamid not implemented') comptype = (tightheader >> 4) & 0x0F if comptype != 9: raise NotImplementedError(f'tight encoding with comptype {comptype} not implemented') compressed_data_length = await self._read_tight_length() compressed_data = await self.reader.readexactly(compressed_data_length) with io.BytesIO(compressed_data) as jpgimg: img = Image.open(jpgimg) img.load() self.framebuffer.paste(img, (x, y)) else: raise Exception(f'Unsupported encoding type: {encoding_type}') async def _read_tight_length(self): length = 0 for i in range(3): byte = await self._read_number(1) length |= ((byte & 0x7F) << (i * 7)) if not (byte & 0x80): break return length async def close(self): self.writer.close() await self.writer.wait_closed()