first commit

This commit is contained in:
kicap 2024-07-27 21:02:06 +07:00
commit 20edac4cdf
15 changed files with 974 additions and 0 deletions

185
.gitignore vendored Normal file
View File

@ -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

8
LICENSE.md Normal file
View File

@ -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.

54
README.md Normal file
View File

@ -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

BIN
city.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

15
main.py Normal file
View File

@ -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()

24
requirements.txt Normal file
View File

@ -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

5
structures/__init__.py Normal file
View File

@ -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

152
structures/app.py Normal file
View File

@ -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]

View File

@ -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

View File

@ -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

185
structures/gui.py Normal file
View File

@ -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('<Configure>', 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))

View File

@ -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)

View File

@ -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

128
structures/protocol.py Normal file
View File

@ -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()

View File

@ -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()