commit 20edac4cdfe050810806edd99f59cad8fc0c770e Author: kicap Date: Sat Jul 27 21:02:06 2024 +0700 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b5c3a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,185 @@ +# Created by .ignore support plugin (hsz.mobi) +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject +### VirtualEnv template +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +.venv +pip-selfcheck.json + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# idea folder, uncomment if you don't need it +.idea \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..eac9b16 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,8 @@ +Copyright 2024 GamesProSeif + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..679c5cf --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +## Remote Desktop App + +This project implements a remote desktop application using Python's Twisted and Tkinter libraries. It offers features for secure connections, fast screen sharing, and out-of-the-box file transfer capabilities. + +### Features + +* **Secure Connection:** Establishes a secure connection between client and server for data transmission. (Details on the specific security mechanisms can be added here if applicable) +* **Fast Screen Sharing:** Enables efficient screen sharing with minimal latency for a smooth remote desktop experience. +* **File Transfer:** Allows users to transfer files between the client and server machines seamlessly. + +### Installation + +1. **Prerequisites:** + - Python 3.x ([https://www.python.org/downloads/](https://www.python.org/downloads/)) + +2. **Install Dependencies:** + ```bash + pip install -r requirements.txt + ``` + This command installs the required libraries from the `requirements.txt` file. Make sure you have `pip` installed (usually included with Python). + +### Usage + +**Running the Application:** + +There are two ways to run the application: + +1. **Normal Mode:** + ```bash + py main.py + ``` + This starts the application in normal mode. + +2. **Debug Mode:** + ```bash + py -d main.py + ``` + This starts the application in debug mode, providing more detailed information about the program's execution. + +### Contributing + +We welcome contributions to this project! If you have any bug fixes, feature improvements, or suggestions, feel free to submit a pull request. + +### License + +This project is licensed under the MIT. You can find the license details in the LICENSE file. + +### Team Members + +- Seif Mansour +- Seif Kazem +- Amr Kamoun +- Mazen Sameh +- Kareem Khamis diff --git a/city.jpg b/city.jpg new file mode 100644 index 0000000..cd77bdd Binary files /dev/null and b/city.jpg differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..44afd8f --- /dev/null +++ b/main.py @@ -0,0 +1,15 @@ +import sys +import asyncio +from structures import App, InputHandling, MouseKeyboardHandler, GUI + +if __name__ == "__main__": + # debug_mode = sys.argv[1] # client | server + app = App() + app.debug = sys.flags.debug # Show connection log to terminal + gui = GUI(app) + app.gui = gui + + try: + gui.start() + except KeyboardInterrupt: + exit() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..62808bc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,24 @@ +attrs==23.2.0 +Automat==22.10.0 +constantly==23.10.4 +hyperlink==21.0.0 +idna==3.7 +incremental==22.10.0 +keyboard==0.13.5 +MouseInfo==0.1.3 +pillow==10.3.0 +PyAutoGUI==0.9.54 +PyGetWindow==0.0.9 +PyMsgBox==1.0.9 +pynput==1.7.7 +pyperclip==1.8.2 +PyRect==0.2.0 +PyScreeze==0.1.30 +pytweening==1.2.0 +screeninfo==0.8.1 +six==1.16.0 +tk==0.1.0 +Twisted==24.3.0 +twisted-iocpsupport==1.0.4 +typing_extensions==4.11.0 +zope.interface==6.4 diff --git a/structures/__init__.py b/structures/__init__.py new file mode 100644 index 0000000..a5c5716 --- /dev/null +++ b/structures/__init__.py @@ -0,0 +1,5 @@ +from .app import App +from .protocol import TCPProtocol, TCPFactory +from .input_handling import InputHandling +from .handling_recieve import MouseKeyboardHandler +from .gui import GUI diff --git a/structures/app.py b/structures/app.py new file mode 100644 index 0000000..fcbd59f --- /dev/null +++ b/structures/app.py @@ -0,0 +1,152 @@ +from pickle import dumps +from random import choice +from time import sleep +from twisted.internet import reactor, tksupport, defer +from twisted.internet.endpoints import TCP4ServerEndpoint, TCP4ClientEndpoint +from structures.protocol import TCPFactory +from structures.input_handling import InputHandling +from structures.handling_recieve import MouseKeyboardHandler +from structures.share_screen_handler import ScreenShareHandler +from .auth_handler import AuthHandler +from .file_handler import FileHandler +from socket import gethostname, gethostbyname_ex +from io import BytesIO + + +class App: + def __init__(self): + self.debug = False + self.mode = None + self.protocol = None + self.screen_share_protocol = None + self._handlers = {} + self._listeners = [] + self._delisteners = [] + self.host = self.get_host() + self.port = 5005 + self.screen_share_port = 5006 + self.running = False + self.pass_code = None + self.authenticated = False + self.auth_handler = AuthHandler(self) + self.screen_share_handler = ScreenShareHandler(self) + self.file_handler = FileHandler(self) + + def setMode(self, mode): + if mode == "server" or mode == "client": + self.mode = mode + else: + raise Exception(f"Invalid mode: {mode}") + + def handlerManagerReceive(self, message): + if message == "file_dialog_open": + self.screen_share_handler.active = False + elif message == "file_dialog_close": + self.screen_share_handler.active = True + + def addHandler(self, event_name, fn): + self._handlers[event_name] = fn + + def useHandler(self, event_name, *args): + self._handlers[event_name](*args) + + def getHandlerNames(self): + return self._handlers.keys() + + def addListener(self, listener): + self._listeners.append(listener) + + def addDelistener(self, delistener): + self._delisteners.append(delistener) + + def listen(self): + for listener in self._listeners: + reactor.callInThread(listener) + + def startGUI(self, gui): + tksupport.install(gui.server_root if self.mode == "server" else gui.client_root) + + def send(self, event, *args): + MAX_MSG_SIZE = 1024 # Assuming MAX_MSG_SIZE is defined elsewhere + + data = dumps({"event": event, "args": args}) + if len(data) <= MAX_MSG_SIZE: + # Message fits within limit, send directly + prefix = f"##HEAD{event:<10}{0:<4}{1:<4}##" + msg = prefix.encode() + data + b"##FRAMEEND##" + self.protocol.transport.write(msg) + else: + # Message exceeds limit, fragment and send + num_chunks = (len(data) // MAX_MSG_SIZE) + 1 # Calculate number of chunks + for chunk_num in range(num_chunks): + start = chunk_num * MAX_MSG_SIZE + end = min(start + MAX_MSG_SIZE, len(data)) + chunk = data[start:end] + + # Prefix with event name and chunk number for reassembly + prefix = f"##HEAD{event:<10}{chunk_num:<4}{num_chunks:<4}##" + msg = prefix.encode() + chunk + b"##FRAMEEND##" + self.protocol.transport.write(msg) + + def start(self): + self.running = True + if self.mode == "server": + mouse_keyboard_handler = MouseKeyboardHandler() + self.addHandler("HANDLER", self.handlerManagerReceive) + self.addHandler("MOUSE", mouse_keyboard_handler.mouse) + self.addHandler("KEYBOARD", mouse_keyboard_handler.keyboard) + self.addHandler("FILE", self.file_handler.receive_file) + self.addListener(self.screen_share_handler.capture_and_send) + + endpoint = TCP4ServerEndpoint(reactor, self.port) + endpoint.listen(TCPFactory(self)) + screen_share_endpoint = TCP4ServerEndpoint(reactor, self.screen_share_port) + screen_share_endpoint.listen(TCPFactory(self, True)) + if self.debug: + print("DEBUG: Started TCP Server") + elif self.mode == "client": + inputhandling = InputHandling(self) + self.addListener(inputhandling.start) + self.addDelistener(inputhandling.stopListening) + self.addHandler("SCREEN", self.screen_share_handler.receive) + + endpoint = TCP4ClientEndpoint(reactor, self.host, self.port) + endpoint.connect(TCPFactory(self)) + screen_share_endpoint = TCP4ClientEndpoint(reactor, self.host, self.screen_share_port) + screen_share_endpoint.connect(TCPFactory(self, True)) + if self.debug: + print("DEBUG: Started TCP Client") + else: + raise Exception(f"Invalid mode: {self.mode}") + + self.listen() + reactor.run() + + def stop(self): + if self.running: + for delistener in self._delisteners: + delistener() + # reactor.getThreadPool().stop() + # self.protocol.transport.loseConnection() + # self.screen_share_protocol.transport.loseConnection() + reactor.stop() + self.running = False + exit() + + def generate_pass_code(self): + digits = "0123456789abcdef" + code = "" + for _ in range(6): + code += choice(digits) + self.pass_code = code + return code + + def get_link(self): + return f"{self.host}?p={self.pass_code}" + + def get_host(self): + ips = gethostbyname_ex(gethostname())[2] + for ip in ips: + if ip.split(".")[2] == "1": + return ip + return ips[1] diff --git a/structures/auth_handler.py b/structures/auth_handler.py new file mode 100644 index 0000000..1842cd5 --- /dev/null +++ b/structures/auth_handler.py @@ -0,0 +1,21 @@ +import re + +class AuthHandler: + def __init__(self, app): + self.app = app + + def validate_link(self, text): + pattern = r"^(?:[0-9]{1,3}\.){3}(?:[0-9]{1,3})\?p=([0-9a-f]{1,})" + match = re.match(pattern, text) + # Additional check for octets within 0-255 range + if match: + ip_bytes = text.split("?p=")[0].split(".") # Split the IP into octets + return all(0 <= int(octet) <= 255 for octet in ip_bytes) + else: + return False + + def check_pass_code(self, code): + if self.app.pass_code.lower() == code.lower(): + self.app.authenticated = True + return True + return False \ No newline at end of file diff --git a/structures/file_handler.py b/structures/file_handler.py new file mode 100644 index 0000000..40620ae --- /dev/null +++ b/structures/file_handler.py @@ -0,0 +1,37 @@ +from time import sleep +from tkinter import filedialog +from os import path, environ, makedirs + +class FileHandler: + def __init__(self, app): + self.app = app + + def choose_file(self): + self.app.send("HANDLER", "file_dialog_open") + sleep(0.5) + filename = filedialog.askopenfilename(title="Select file to transfer") + sleep(0.5) + self.app.send("HANDLER", "file_dialog_close") + + if filename: + self.send_file_to_server(filename) + + def send_file_to_server(self, filepath): + with open(filepath, "rb") as file: + data = file.read() + filename = filepath.split("/")[-1] + # self.app.protocol.transport.write("stop_share_screen".encode()) + # sleep(1) + self.app.send("FILE", filename, data) + + def receive_file(self, filename, data): + downloads_folder = path.join("C:", environ.get('HOMEPATH'), 'Downloads') + + file_path = path.join(downloads_folder, filename) + + makedirs(downloads_folder, exist_ok=True) + + with open(file_path, 'wb') as file: + file.write(data) + + self.app.screen_share_handler.active = True \ No newline at end of file diff --git a/structures/gui.py b/structures/gui.py new file mode 100644 index 0000000..2d41c6d --- /dev/null +++ b/structures/gui.py @@ -0,0 +1,185 @@ +import tkinter as tk +from PIL import Image, ImageTk +from pyperclip import copy as copy_to_clipboard +from .protocol import is_server_listening + + +class GUI: + def __init__(self, app): + self.app = app + self.screen_dimensions = [0, 0, 0, 0] + self.focused = False + + self.geometry = "800x600" + + self.root = tk.Tk() + self.root.title("Remote Desktop App | Muh ALif Basri (1217 280 225)") + self.root.geometry(self.geometry) + + self.main_frame = tk.Frame(self.root) + self.main_frame.pack(fill='both', expand=True) + + self.label3=tk.Label(self.main_frame,text="Pilih Server Atau Client",relief="solid",font=("arial",16,"bold")) + self.label3.pack(pady=20) + + self.btn_server = tk.Button(self.main_frame, text="Server",fg='blue',bg='white',relief="ridge",command=self.show_server_page) + self.btn_server.pack(pady=20) + + self.btn_client = tk.Button(self.main_frame, text="Client",fg='blue',bg='white',relief="ridge",command=self.show_client_page) + self.btn_client.pack(pady=20) + + self.client_frame = tk.Frame(self.root) + self.link_entry = tk.Entry(self.client_frame) + self.link_entry.pack(pady=20, padx=100, fill='x', expand=True) + + self.connect_button = tk.Button(self.client_frame, text="Connect", command=self.is_server_alive) + self.connect_button.pack(pady=20) + + + # self.root.protocol("WM_DELETE_WINDOW", self.on_window_close) + + def init_server_gui(self): + self.server_root = tk.Tk() + self.server_root.title("Remote Desktop App | Server") + self.server_root.geometry(self.geometry) + + self.server_frame = tk.Frame(self.server_root) + + self.label4=tk.Label(self.server_frame,text="Share Link",fg='blue',bg='white',font=("arial",16,"bold")) + self.label4.pack(pady=100) + + self.password_entry = tk.Entry(self.server_frame) + self.password_entry.pack(pady=20, padx=50, fill='x', expand=True) + + self.copy_link_button = tk.Button(self.server_frame, text="Copy Link", command=self.copy_link) + self.copy_link_button.pack(pady=20) + self.server_root.protocol("WM_DELETE_WINDOW", self.on_window_close) + + def init_client_gui(self): + self.client_root = tk.Tk() + self.client_root.title("Remote Desktop App | Client") + self.client_root.geometry(self.geometry) + + self.connected_client_page = tk.Frame(self.client_root) + self.connected_client_page.bind('', self.adjust_image) # Bind resize event + + self.tools_frame = tk.Frame(self.connected_client_page) + self.tools_frame.pack(fill='x') + + self.disconnect_button = tk.Button(self.tools_frame, text="Disconnect", command=self.disconnect) + self.disconnect_button.pack(side='left', padx=10) + + self.file_transfer_button = tk.Button(self.tools_frame, text="File Transfer", command=self.app.file_handler.choose_file) + self.file_transfer_button.pack(side='left', padx=10) + + # self.chat_button = tk.Button(self.tools_frame, text="Chat") + # self.chat_button.pack(side='left', padx=10) + + self.original_image = Image.open("city.jpg") + + self.screen_area = tk.Label(self.connected_client_page) + self.screen_area.pack(fill='both', expand=True) + self.client_root.protocol("WM_DELETE_WINDOW", self.on_window_close) + + def start(self): + self.root.mainloop() + + def adjust_image(self, event=None): + # Calculating new size maintaining 16:9 aspect ratio + new_width = self.connected_client_page.winfo_width() + if new_width < 5: + new_width = 800 + new_height = int(new_width * 9 / 16) + + # Resize the image using Pillow + resized_image = self.original_image.resize((new_width, new_height), Image.Resampling.LANCZOS) + photo_image = ImageTk.PhotoImage(resized_image) + + # Update the label image + self.screen_area.config(image=photo_image) + self.screen_area.image = photo_image # Keep a reference! + self.set_screen_dimensions(new_height) + self.focused = self.client_root == self.client_root.focus_get() + + def set_screen_dimensions(self, img_height): + geometry_string = self.screen_area.winfo_geometry() + offset_list = geometry_string.split("+") + if len(offset_list) != 3: + return None + + rootx = self.client_root.winfo_rootx() + rooty = self.client_root.winfo_rooty() + + [width, height] = [int(num) for num in offset_list[0].split("x")] + xmin = int(offset_list[1]) + rootx + ymin = int(offset_list[2]) + rooty + xmax = xmin + width + ymax = ymin + height + + ymin += (height - img_height) / 2 + ymax -= (height - img_height) / 2 + + self.screen_dimensions = [xmin, ymin, xmax, ymax] + + def disconnect(self): + self.client_root.destroy() + self.on_window_close() + + def show_server_page(self): + self.app.setMode("server") + self.main_frame.pack_forget() + self.root.destroy() + self.init_server_gui() + self.app.startGUI(self) + self.server_frame.pack(fill='both', expand=True) + + self.app.generate_pass_code() + self.password_entry.delete(0, tk.END) + self.password_entry.insert(0, self.app.get_link()) + + self.app.start() + + def show_client_page(self): + self.main_frame.pack_forget() + self.client_frame.pack(fill='both', expand=True) + + def copy_link(self): + copy_to_clipboard(self.app.get_link()) + self.change_button_text_temp(self.server_root, self.copy_link_button, "Copied to Clipboard") + + def is_server_alive(self): + link = self.link_entry.get() + + # validate link + valid = self.app.auth_handler.validate_link(link) + + if valid: + [ip, pass_code] = link.split("?p=") + if self.app.debug: + print(f"DEBUG: Attempting to connect to {link}") + is_alive = is_server_listening(ip, self.app.port, pass_code) + if is_alive: + self.app.host = ip + self.connect_to_server() + else: + self.change_button_text_temp(self.root, self.connect_button, "Server not Active") + else: + self.change_button_text_temp(self.root, self.connect_button, "Invalid Link") + + def connect_to_server(self): + self.app.setMode("client") + self.client_frame.pack_forget() + self.root.destroy() + self.init_client_gui() + self.app.startGUI(self) + self.connected_client_page.pack(fill='both', expand=True) + self.adjust_image() + self.app.start() + + def on_window_close(self): + self.app.stop() + + def change_button_text_temp(self, root, button, new_text): + old_text = button.config()["text"][4] + button.config(text=new_text) + root.after(1500, lambda: button.config(text=old_text)) diff --git a/structures/handling_recieve.py b/structures/handling_recieve.py new file mode 100644 index 0000000..29f02a6 --- /dev/null +++ b/structures/handling_recieve.py @@ -0,0 +1,60 @@ +import pyautogui +from screeninfo import get_monitors + +[screenWidth, screenHeight] = [ + [screen.width, screen.height] for screen in get_monitors() if screen.is_primary +][0] + +class MouseKeyboardHandler: + @staticmethod + def mouse(event_type, *args): + if event_type == "CLICK": + if args[0] == "Right": + pyautogui.click(button='right') + else: + pyautogui.click(button='left') + elif event_type == "SCROLL": + if args[0] > 0: + pyautogui.scroll(200) + else: + pyautogui.scroll(-200) + elif event_type == "MOVE": + x, y = args[:2] + physicalX = x * screenWidth + physicalY = y * screenHeight + pyautogui.moveTo(physicalX, physicalY, duration=0.1) + + @staticmethod + def keyboard(string): + special_keys = { + "enter": "enter", + "backspace": "backspace", + "space": "space", + "esc": "esc", + "ctrl": "ctrl", + "alt": "alt", + "shift": "shift", + "up": "up", + "down": "down", + "left": "left", + "right": "right", + "f1": "f1", + "f2": "f2", + "f3": "f3", + "f4": "f4", + "f5": "f5", + "f6": "f6", + "f7": "f7", + "f8": "f8", + "f9": "f9", + "f10": "f10", + "f11": "f11", + "f12": "f12", + "f13": "f13", + } + + if string in special_keys: + pyautogui.press(special_keys[string]) + else: + pyautogui.typewrite(string) + diff --git a/structures/input_handling.py b/structures/input_handling.py new file mode 100644 index 0000000..64ee6ee --- /dev/null +++ b/structures/input_handling.py @@ -0,0 +1,68 @@ +from pynput.mouse import Listener as MouseListener, Button +from time import time +import keyboard + + +class InputHandling: + def __init__(self, app): + self.app = app + self.last_mouse_move_time = time() + + def start(self): + def get_coords(x, y): + if time() - self.last_mouse_move_time < 0.5: + return + [xmin, ymin, xmax, ymax] = self.app.gui.screen_dimensions + if self.app.gui.focused and x > xmin and x < xmax and y > ymin and y < ymax: + if self.app.debug: + print("DEBUG: Mouse ", x, y) + relativeX = (x - xmin) / (xmax - xmin) + relativeY = (y - ymin) / (ymax - ymin) + self.app.send("MOUSE", "MOVE", relativeX, relativeY) + self.last_mouse_move_time = time() + + def on_key_event(event): + if not self.app.gui.focused: + return + if self.app.debug: + print("DEBUG: Keyboard", event.name) + self.app.send("KEYBOARD", event.name) + + def on_click(x, y, button, pressed): + [xmin, ymin, xmax, ymax] = self.app.gui.screen_dimensions + if self.app.gui.focused and x > xmin and x < xmax and y > ymin and y < ymax: + if pressed: + if button == Button.left: + button_name = 'Left' + elif button == Button.right: + button_name = 'Right' + elif button == Button.middle: + button_name = 'Middle' + else: + button_name = 'Unknown' + return + + if self.app.debug: + print("DEBUG: Mouse Click", button_name) + self.app.send("MOUSE", "CLICK", button_name) + + def on_scroll(x, y, dx, dy): + [xmin, ymin, xmax, ymax] = self.app.gui.screen_dimensions + if self.app.gui.focused and x > xmin and x < xmax and y > ymin and y < ymax: + if self.app.debug: + print(f"DEBUG: Mouse scrolled at: ({dx}, {dy})") + self.app.send("MOUSE", "SCROLL", dy) + + # Register listeners outside the start function + mouse_listener = MouseListener(on_move=get_coords, on_click=on_click, on_scroll=on_scroll) + self.mouse_listener = mouse_listener + mouse_listener.start() + + keyboard.on_press(on_key_event) # Register keyboard listener + + keyboard.wait('esc') # Wait for ESC key press + self.stopListening() + + def stopListening(self): + self.mouse_listener.stop() + keyboard.unhook_all() # Unregister listeners diff --git a/structures/protocol.py b/structures/protocol.py new file mode 100644 index 0000000..39f06b7 --- /dev/null +++ b/structures/protocol.py @@ -0,0 +1,128 @@ +import socket +from twisted.internet.protocol import Protocol, connectionDone, Factory +from pickle import loads, UnpicklingError +from io import BytesIO + + +class TCPProtocol(Protocol): + def __init__(self, app, is_screen_share = False): + self.app = app + self.is_screen_share = is_screen_share + if is_screen_share: + self.app.screen_share_protocol = self + else: + self.app.protocol = self + self.buffer = {} + self.buffer["MAIN"] = BytesIO() + for key in self.app.getHandlerNames(): + self.buffer[key] = BytesIO() + + def connectionMade(self): + if self.app.debug: + print("DEBUG: NEW_CONNECTION -", "SCREEN" if self.is_screen_share else "MAIN") + + def dataReceived(self, data: bytes): + # print("rec-size:", len(data)) + # print("HEAD:", data[:40]) + try: + # if data.decode() == self.app.pass_code: + if data.decode() == "stop_screen_share": + self.app.screen_share_handler.active = False + elif self.app.auth_handler.check_pass_code(data.decode()): + return self.transport.write("ok".encode()) + else: + raise Exception() + except Exception: + try: + # print("PREVHEAD:", self.buffer["MAIN"].getvalue()[:40]) + self.buffer["MAIN"].seek(len(self.buffer["MAIN"].getvalue())) + self.buffer["MAIN"].write(data) + while True: + # print("HEAD:", self.buffer["MAIN"].getvalue()[:40]) + start_index = self.buffer["MAIN"].getvalue().find(b"##HEAD") + end_index = self.buffer["MAIN"].getvalue().find(b"##FRAMEEND##") + if end_index == -1: + break + event_name = self.buffer["MAIN"].getvalue()[start_index+6:start_index+16].strip().decode() + chunk_num = int(self.buffer["MAIN"].getvalue()[start_index+16:start_index+20].strip()) + chunk_max = int(self.buffer["MAIN"].getvalue()[start_index+20:start_index+24].strip()) + encoded_data = self.buffer["MAIN"].getvalue()[start_index+26:end_index] + # print(event_name, "start:", start_index, "end:", end_index, "chunk_num:", chunk_num, "chunk_max:", chunk_max) + if start_index != 0: + raise Exception("startindex is not 0") + # print("rec:" ,len(self.buffer[event_name].getvalue()), delimiter_index) + + self.buffer["MAIN"] = BytesIO(self.buffer["MAIN"].getvalue()[end_index + len(b"##FRAMEEND##"):]) + # print("NEXT_HEAD:", self.buffer["MAIN"].getvalue()[:40]) + + self.buffer[event_name].write(encoded_data) + + if chunk_num + 1 != chunk_max: + continue + + msg = loads(self.buffer[event_name].getvalue()) + event = msg["event"] + args = msg["args"] + self.app.useHandler(event, *args) + + if self.app.debug: + if len(self.buffer[event_name].getvalue()) < 1000: + print(f"DEBUG: RECEIVED {event} - {msg}") + else: + print(f"DEBUG: RECEIVED {event} - too long to show") + + self.buffer[event_name].seek(0) + self.buffer[event_name].truncate() + + # if self.buffer[event_name].getvalue().find(b"stop_share_screen") != -1: + # print("FOUND_HERE", self.buffer[event_name].getvalue().find(b"stop_share_screen")) + # delimiter_index = self.buffer[event_name].getvalue().find(b"##FRAMEEND##") + # print("rec:" ,len(self.buffer[event_name].getvalue()), delimiter_index) + # if delimiter_index != -1: + # msg = loads(self.buffer[event_name].getvalue()[:delimiter_index]) + # event = msg["event"] + # args = msg["args"] + # self.app.useHandler(event, *args) + + # if self.app.debug: + # if len(self.buffer[event_name].getvalue()) < 1000: + # print(f"DEBUG: RECEIVED {event} - {msg}") + # else: + # print(f"DEBUG: RECEIVED {event} - too long to show") + + # self.buffer[event_name] = BytesIO(self.buffer[event_name].getvalue()[delimiter_index + len(b"##FRAMEEND##"):]) + except Exception as exception: + if self.is_screen_share: + # self.buffer["SCREEN"] = BytesIO(self.buffer["SCREEN"].getvalue()[delimiter_index + len(b"##FRAMEEND##"):]) + self.buffer.seek(0) + self.buffer.truncate() + else: + raise exception + + def connectionLost(self, reason=connectionDone): + if self.app.debug: + print(f"DEBUG: CONNECTION_LOST - { 'SCREEN' if self.is_screen_share else 'MAIN '} - {reason.value}") + + +class TCPFactory(Factory): + def __init__(self, app, is_screen_share = False): + self.app = app + self.is_screen_share = is_screen_share + + def buildProtocol(self, addr): + return TCPProtocol(self.app, self.is_screen_share) + +def is_server_listening(host, port, code): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.settimeout(1) + result = sock.connect_ex((host, port)) + if result == 0: + sock.sendall(code.encode()) + res = sock.recv(1024).decode() + return res.strip() == "ok" + return False + except socket.timeout: + return False + finally: + sock.close() diff --git a/structures/share_screen_handler.py b/structures/share_screen_handler.py new file mode 100644 index 0000000..5abd175 --- /dev/null +++ b/structures/share_screen_handler.py @@ -0,0 +1,32 @@ +from time import sleep +from PIL import Image +from io import BytesIO +import pyscreeze + + +class ScreenShareHandler: + def __init__(self, app): + self.app = app + self.screen = pyscreeze + self.active = True + self.frame_rate = 15 # frames per second + + def capture_and_send(self): + delay = 1 / self.frame_rate + while self.app.running: + if self.app.authenticated and self.active: + frame = self.screen.screenshot() + frame_rgb = frame.convert("RGB") + buffer = BytesIO() + frame_rgb.save(buffer, "JPEG", quality=70) + compressed_frame_data = buffer.getvalue() + self.app.send("SCREEN", compressed_frame_data, frame_rgb.width, frame_rgb.height) + sleep(delay) + + def receive(self, frame, width, height): + # Known issue: Interleaved Data Streams + if not isinstance(frame, bytes): + frame = bytes(frame) + frame_buffer = BytesIO(frame) + self.app.gui.original_image = Image.open(frame_buffer) + self.app.gui.adjust_image()