diff options
-rw-r--r-- | poetry.lock | 194 | ||||
-rw-r--r-- | pyproject.toml | 6 | ||||
-rw-r--r-- | service.py | 81 | ||||
-rw-r--r-- | session.py | 91 | ||||
-rw-r--r-- | test_client.py | 10 | ||||
-rw-r--r-- | vnc.py | 32 |
6 files changed, 412 insertions, 2 deletions
diff --git a/poetry.lock b/poetry.lock index 12fbad9..0e68b69 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,7 +1,197 @@ -package = [] +[[package]] +category = "main" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2020.6.20" + +[[package]] +category = "main" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "main" +description = "Python bindings for libdbus" +name = "dbus-python" +optional = false +python-versions = "*" +version = "1.2.16" + +[[package]] +category = "main" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.10" + +[[package]] +category = "main" +description = "NumPy is the fundamental package for array computing with Python." +name = "numpy" +optional = false +python-versions = ">=3.5" +version = "1.18.5" + +[[package]] +category = "main" +description = "Utility that helps with local TCP ports managment. It can find an unused TCP localhost port and remember the association." +name = "port-for" +optional = false +python-versions = "*" +version = "0.4" + +[[package]] +category = "main" +description = "Cross-platform lib for process and system monitoring in Python." +name = "psutil" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "5.7.2" + +[package.extras] +test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"] + +[[package]] +category = "main" +description = "Python interface for cairo" +name = "pycairo" +optional = false +python-versions = ">=3.5, <4" +version = "1.19.1" + +[[package]] +category = "main" +description = "Python bindings for GObject Introspection" +name = "pygobject" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "3.36.1" + +[package.dependencies] +pycairo = ">=1.11.1" + +[[package]] +category = "main" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.24.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[[package]] +category = "main" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "1.25.9" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + +[[package]] +category = "main" +description = "Websockify." +name = "websockify" +optional = false +python-versions = "*" +version = "0.9.0" + +[package.dependencies] +numpy = "*" [metadata] -content-hash = "8165d934e932435bf4742b9198674202413b43524911713d5c7c55cb8d314618" +content-hash = "647d3ef2f88b9c37462804e88709259cfabccd8b431cb5d998100c86e611819a" python-versions = "^3.5" [metadata.files] +certifi = [ + {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, + {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +dbus-python = [ + {file = "dbus-python-1.2.16.tar.gz", hash = "sha256:11238f1d86c995d8aed2e22f04a1e3779f0d70e587caffeab4857f3c662ed5a4"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +numpy = [ + {file = "numpy-1.18.5-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:e91d31b34fc7c2c8f756b4e902f901f856ae53a93399368d9a0dc7be17ed2ca0"}, + {file = "numpy-1.18.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7d42ab8cedd175b5ebcb39b5208b25ba104842489ed59fbb29356f671ac93583"}, + {file = "numpy-1.18.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a78e438db8ec26d5d9d0e584b27ef25c7afa5a182d1bf4d05e313d2d6d515271"}, + {file = "numpy-1.18.5-cp35-cp35m-win32.whl", hash = "sha256:a87f59508c2b7ceb8631c20630118cc546f1f815e034193dc72390db038a5cb3"}, + {file = "numpy-1.18.5-cp35-cp35m-win_amd64.whl", hash = "sha256:965df25449305092b23d5145b9bdaeb0149b6e41a77a7d728b1644b3c99277c1"}, + {file = "numpy-1.18.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ac792b385d81151bae2a5a8adb2b88261ceb4976dbfaaad9ce3a200e036753dc"}, + {file = "numpy-1.18.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:ef627986941b5edd1ed74ba89ca43196ed197f1a206a3f18cc9faf2fb84fd675"}, + {file = "numpy-1.18.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f718a7949d1c4f622ff548c572e0c03440b49b9531ff00e4ed5738b459f011e8"}, + {file = "numpy-1.18.5-cp36-cp36m-win32.whl", hash = "sha256:4064f53d4cce69e9ac613256dc2162e56f20a4e2d2086b1956dd2fcf77b7fac5"}, + {file = "numpy-1.18.5-cp36-cp36m-win_amd64.whl", hash = "sha256:b03b2c0badeb606d1232e5f78852c102c0a7989d3a534b3129e7856a52f3d161"}, + {file = "numpy-1.18.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7acefddf994af1aeba05bbbafe4ba983a187079f125146dc5859e6d817df824"}, + {file = "numpy-1.18.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cd49930af1d1e49a812d987c2620ee63965b619257bd76eaaa95870ca08837cf"}, + {file = "numpy-1.18.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b39321f1a74d1f9183bf1638a745b4fd6fe80efbb1f6b32b932a588b4bc7695f"}, + {file = "numpy-1.18.5-cp37-cp37m-win32.whl", hash = "sha256:cae14a01a159b1ed91a324722d746523ec757357260c6804d11d6147a9e53e3f"}, + {file = "numpy-1.18.5-cp37-cp37m-win_amd64.whl", hash = "sha256:0172304e7d8d40e9e49553901903dc5f5a49a703363ed756796f5808a06fc233"}, + {file = "numpy-1.18.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e15b382603c58f24265c9c931c9a45eebf44fe2e6b4eaedbb0d025ab3255228b"}, + {file = "numpy-1.18.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3676abe3d621fc467c4c1469ee11e395c82b2d6b5463a9454e37fe9da07cd0d7"}, + {file = "numpy-1.18.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4674f7d27a6c1c52a4d1aa5f0881f1eff840d2206989bae6acb1c7668c02ebfb"}, + {file = "numpy-1.18.5-cp38-cp38-win32.whl", hash = "sha256:9c9d6531bc1886454f44aa8f809268bc481295cf9740827254f53c30104f074a"}, + {file = "numpy-1.18.5-cp38-cp38-win_amd64.whl", hash = "sha256:3dd6823d3e04b5f223e3e265b4a1eae15f104f4366edd409e5a5e413a98f911f"}, + {file = "numpy-1.18.5.zip", hash = "sha256:34e96e9dae65c4839bd80012023aadd6ee2ccb73ce7fdf3074c62f301e63120b"}, +] +port-for = [ + {file = "port-for-0.4.tar.gz", hash = "sha256:47b5cb48f8e036497cd73b96de305cecb4070e9ecbc908724afcbd2224edccde"}, + {file = "port_for-0.4-py2.py3-none-any.whl", hash = "sha256:247b4db1901aa3d9906258308e40dfbadf65275b27ca77faa0b9a876b7284970"}, +] +psutil = [ + {file = "psutil-5.7.2-cp27-none-win32.whl", hash = "sha256:f2018461733b23f308c298653c8903d32aaad7873d25e1d228765e91ae42c3f2"}, + {file = "psutil-5.7.2-cp27-none-win_amd64.whl", hash = "sha256:66c18ca7680a31bf16ee22b1d21b6397869dda8059dbdb57d9f27efa6615f195"}, + {file = "psutil-5.7.2-cp35-cp35m-win32.whl", hash = "sha256:5e9d0f26d4194479a13d5f4b3798260c20cecf9ac9a461e718eb59ea520a360c"}, + {file = "psutil-5.7.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4080869ed93cce662905b029a1770fe89c98787e543fa7347f075ade761b19d6"}, + {file = "psutil-5.7.2-cp36-cp36m-win32.whl", hash = "sha256:d8a82162f23c53b8525cf5f14a355f5d1eea86fa8edde27287dd3a98399e4fdf"}, + {file = "psutil-5.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:0ee3c36428f160d2d8fce3c583a0353e848abb7de9732c50cf3356dd49ad63f8"}, + {file = "psutil-5.7.2-cp37-cp37m-win32.whl", hash = "sha256:ff1977ba1a5f71f89166d5145c3da1cea89a0fdb044075a12c720ee9123ec818"}, + {file = "psutil-5.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a5b120bb3c0c71dfe27551f9da2f3209a8257a178ed6c628a819037a8df487f1"}, + {file = "psutil-5.7.2-cp38-cp38-win32.whl", hash = "sha256:10512b46c95b02842c225f58fa00385c08fa00c68bac7da2d9a58ebe2c517498"}, + {file = "psutil-5.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:68d36986ded5dac7c2dcd42f2682af1db80d4bce3faa126a6145c1637e1b559f"}, + {file = "psutil-5.7.2.tar.gz", hash = "sha256:90990af1c3c67195c44c9a889184f84f5b2320dce3ee3acbd054e3ba0b4a7beb"}, +] +pycairo = [ + {file = "pycairo-1.19.1.tar.gz", hash = "sha256:2c143183280feb67f5beb4e543fd49990c28e7df427301ede04fc550d3562e84"}, +] +pygobject = [ + {file = "PyGObject-3.36.1.tar.gz", hash = "sha256:012a589aec687bfa809a1ff9f5cd775dc7f6fcec1a6bc7fe88e1002a68f8ba34"}, +] +requests = [ + {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, + {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, +] +urllib3 = [ + {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"}, + {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"}, +] +websockify = [ + {file = "websockify-0.9.0.tar.gz", hash = "sha256:c35b5b79ebc517d3b784dacfb993be413a93cda5222c6f382443ce29c1a6cada"}, +] diff --git a/pyproject.toml b/pyproject.toml index 8fffb40..acdfc6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,12 @@ license = "GPL-2.0-or-later" [tool.poetry.dependencies] python = "^3.5" +dbus-python = "^1.2.16" +PyGObject = "^3.36.1" +port_for = "^0.4" +requests = "^2.24.0" +websockify = "^0.9.0" +psutil = "^5.7.2" [tool.poetry.dev-dependencies] diff --git a/service.py b/service.py new file mode 100644 index 0000000..6125a75 --- /dev/null +++ b/service.py @@ -0,0 +1,81 @@ +import json +import time +from threading import Thread + +import dbus +import dbus.service + +from session import Session + + +class RWAService(dbus.service.Object): + def __init__(self): + self.bus = dbus.SessionBus() + name = dbus.service.BusName("de.rwa.rwa", bus=self.bus) + + self.update_service_running = False + self.sessions = {} + super().__init__(name, "/RWA") + + @dbus.service.method("de.rwa.rwa", out_signature="s") + def start(self): + """Start a new remote session.""" + # Start session + session = Session() + + # Add session to sessions list + self.sessions[session.pid] = session + + # Start session update service + self._ensure_update_service() + + return json.dumps(session.client_meta) + + @dbus.service.method("de.rwa.rwa", in_signature="i") + def stop(self, pid: int): + """Stop a remote session.""" + session = self.sessions[pid] + session.stop() + + def _ensure_update_service(self): + """Start session update thread if it isn't already running.""" + if not self.update_service_running: + self.update_thread = Thread(target=self._update_sessions) + self.update_thread.start() + + def _update_sessions(self): + """Go through all running sessions and update their status. + + Things that this function will do: + - Check if VNC is still running + - Kill websockify if VNC process is dead + """ + while len(self.sessions.values()) > 0: + for session in list(self.sessions.values()): + print(f"Session #{session.pid}") + + # Check if VNC process is still running + running = session.vnc_process_running + if running: + print("Session is running") + else: + print("Session is dead.") + + session.stop() + del self.sessions[session.pid] + + time.sleep(2) + + self.update_service_running = False + # TODO Probably kill daemon here (quit main loop) + + +if __name__ == "__main__": + import dbus.mainloop.glib + from gi.repository import GLib + + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + + loop = GLib.MainLoop() + object = RWAService() + loop.run() diff --git a/session.py b/session.py new file mode 100644 index 0000000..3c8eeb9 --- /dev/null +++ b/session.py @@ -0,0 +1,91 @@ +import os +import secrets +import signal + +import psutil +import requests + +from vnc import run_vnc, save_password + +API_SERVER = "http://127.0.0.1:8000" +REGISTER_URL = API_SERVER + "/app/rwa/api/register/" + + +class Session: + def __init__(self): + self._generate_password() + self._start_vnc() + self._register_session() + + @property + def pid(self) -> int: + return self.vnc_pid + + @property + def port(self) -> int: + return self.ws_port + + def _generate_password(self): + """Generate password for x11vnc and save it.""" + self.password = secrets.token_urlsafe(20) + self.pw_filename = save_password(self.password) + + def _start_vnc(self): + """Start x11vnc server.""" + process_info = run_vnc(self.pw_filename) + + self.vnc_pid = process_info["vnc"]["pid"] + self.vnc_port = process_info["vnc"]["port"] + self.ws_pid = process_info["ws"]["pid"] + self.ws_port = process_info["ws"]["port"] + + def _register_session(self): + """Register session in RWA.""" + r = requests.post( + REGISTER_URL, + json={ + "port": self.ws_port, + "password": self.password, + "pid": self.vnc_pid, + }, + ) + print(r) + self.meta = r.json() + self.session_id = self.meta["session_id"] + self.web_url = self.meta["url"] + self.api_token = self.meta["token"] + self.pin = self.meta["pin"] + + def stop(self): + """Stop session and clean up.""" + # Kill VNC + if self.vnc_pid in psutil.pids(): + print("Kill VNC.") + os.kill(self.vnc_pid, signal.SIGTERM) + + # Kill websockify + if self.ws_pid in psutil.pids(): + print("Kill websockify.") + os.kill(self.ws_pid, signal.SIGTERM) + + # Delete PW file + if os.path.exists(self.pw_filename): + print("Delete password file") + os.remove(self.pw_filename) + + # Delete self + del self + + @property + def vnc_process_running(self): + """Check if the VNC process is still running.""" + if self.vnc_pid in psutil.pids(): + p = psutil.Process(self.vnc_pid) + if p.status() == "zombie": + return False + return True + return False + + @property + def client_meta(self): + return {"id": self.pid, "url": self.web_url, "pin": self.pin} diff --git a/test_client.py b/test_client.py new file mode 100644 index 0000000..cd7d338 --- /dev/null +++ b/test_client.py @@ -0,0 +1,10 @@ +import dbus + +bus = dbus.SessionBus() + + +time = bus.get_object("de.rwa.rwa", "/RWA") + + +curr = time.start() +print("Your VNC session is", curr) @@ -0,0 +1,32 @@ +import os +import subprocess +from typing import Dict +from uuid import uuid4 + +import port_for + + +def save_password(pw: str) -> str: + """Save password in x11vnc format in temporary directory.""" + filename = f"/tmp/rwa/{uuid4()}.pw" + os.makedirs("/tmp/rwa/", exist_ok=True) + p = subprocess.Popen(["x11vnc", "-storepasswd", f"{pw}", filename]) + p.communicate() + return filename + + +def run_vnc(pw_filename: str) -> Dict[str, Dict[str, int]]: + """Run x11vnc and websockify with random, unique ports in background.""" + port = port_for.select_random() + port_vnc = port_for.select_random() + + # Start VNC process + p = subprocess.Popen(["x11vnc", "-rfbauth", pw_filename, "-rfbport", f"{port_vnc}"]) + + # Start websockify + p2 = subprocess.Popen(f"websockify {port} 127.0.0.1:{port_vnc}", shell=True,) + + return { + "ws": {"pid": p2.pid, "port": port}, + "vnc": {"port": port_vnc, "pid": p.pid}, + } |