From ed1d15aa02b3c7c1350a5204861d1f8678550fbb Mon Sep 17 00:00:00 2001 From: Jonathan Weth Date: Wed, 23 Jun 2021 12:33:28 +0200 Subject: Restructure project directory and provide script for session service --- config.py | 30 --- docs/admin/01_installation.rst | 5 - docs/admin/03_testing.rst | 2 +- lock.py | 50 ----- log.py | 28 --- pyproject.toml | 10 + rwa/support/sessionservice/config.py | 30 +++ rwa/support/sessionservice/lock.py | 50 +++++ rwa/support/sessionservice/log.py | 28 +++ rwa/support/sessionservice/service.py | 340 ++++++++++++++++++++++++++++++++++ rwa/support/sessionservice/session.py | 301 ++++++++++++++++++++++++++++++ rwa/support/sessionservice/trigger.py | 80 ++++++++ rwa/support/sessionservice/vnc.py | 57 ++++++ service.py | 337 --------------------------------- session.py | 301 ------------------------------ trigger.py | 80 -------- vnc.py | 57 ------ 17 files changed, 897 insertions(+), 889 deletions(-) delete mode 100644 config.py delete mode 100644 lock.py delete mode 100644 log.py create mode 100644 rwa/support/sessionservice/config.py create mode 100644 rwa/support/sessionservice/lock.py create mode 100644 rwa/support/sessionservice/log.py create mode 100755 rwa/support/sessionservice/service.py create mode 100644 rwa/support/sessionservice/session.py create mode 100644 rwa/support/sessionservice/trigger.py create mode 100644 rwa/support/sessionservice/vnc.py delete mode 100755 service.py delete mode 100644 session.py delete mode 100644 trigger.py delete mode 100644 vnc.py diff --git a/config.py b/config.py deleted file mode 100644 index f0f8369..0000000 --- a/config.py +++ /dev/null @@ -1,30 +0,0 @@ -# This file is part of Remote Support Desktop -# https://gitlab.das-netzwerkteam.de/RemoteWebApp/rwa.support.sessionservice -# Copyright 2020, 2021 Jonathan Weth -# Copyright 2020 Daniel Teichmann -# Copyright 2020 Mike Gabriel -# SPDX-License-Identifier: GPL-2.0-or-later -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the -# Free Software Foundation, Inc., -# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from dynaconf import Dynaconf - -settings = Dynaconf( - envvar_prefix="RWA", settings_files=["/etc/rwa/support/sessionservice/settings.toml"] -) diff --git a/docs/admin/01_installation.rst b/docs/admin/01_installation.rst index 4b5b45a..461cdf7 100644 --- a/docs/admin/01_installation.rst +++ b/docs/admin/01_installation.rst @@ -44,8 +44,3 @@ Now you can continue with :doc:`02_config`. :: poetry run python test_client.py - - # or - - poetry run python tox - diff --git a/docs/admin/03_testing.rst b/docs/admin/03_testing.rst index ab427bc..7e447c4 100644 --- a/docs/admin/03_testing.rst +++ b/docs/admin/03_testing.rst @@ -12,7 +12,7 @@ so the client can interact with it: :: - poetry run python service.py + poetry run rwa-support-sessionservice Now you can use the test client to start a test session and register it in your RWA server. It will return a PIN and diff --git a/lock.py b/lock.py deleted file mode 100644 index b4e384c..0000000 --- a/lock.py +++ /dev/null @@ -1,50 +0,0 @@ -# This file is part of Remote Support Desktop -# https://gitlab.das-netzwerkteam.de/RemoteWebApp/rwa.support.sessionservice -# Copyright 2020, 2021 Jonathan Weth -# Copyright 2020 Daniel Teichmann -# Copyright 2020 Mike Gabriel -# SPDX-License-Identifier: GPL-2.0-or-later -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the -# Free Software Foundation, Inc., -# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os -import tempfile - -lock_dir_path = os.path.join(tempfile.gettempdir(), "rwa.support.sessionservice") -lock_file_path = os.path.join(lock_dir_path, "rwa.support.sessionservice.lock") - - -def lock(): - """Create lock file.""" - os.makedirs(lock_dir_path, exist_ok=True) - with open(lock_file_path, "w") as f: - f.write("lock") - - -def is_locked(): - """Check if the lock file exists.""" - return os.path.exists(lock_file_path) - - -def unlock(): - """Remove the lock file.""" - try: - os.remove(lock_file_path) - except FileNotFoundError: - pass diff --git a/log.py b/log.py deleted file mode 100644 index ad8bf88..0000000 --- a/log.py +++ /dev/null @@ -1,28 +0,0 @@ -# This file is part of Remote Support Desktop -# https://gitlab.das-netzwerkteam.de/RemoteWebApp/rwa.support.sessionservice -# Copyright 2020, 2021 Jonathan Weth -# Copyright 2020 Daniel Teichmann -# Copyright 2020 Mike Gabriel -# SPDX-License-Identifier: GPL-2.0-or-later -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the -# Free Software Foundation, Inc., -# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import logging - -logging.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", level=logging.INFO) diff --git a/pyproject.toml b/pyproject.toml index 19c48e6..b742893 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,10 @@ authors = [ "Jonathan Weth " ] license = "GPL-2.0-or-later" +packages = [ + { include = "rwa/" }, +] +include = ["README.rst", "LICENCE", "org.ArcticaProject.RWASupportSessionService.service", "docs/*", "docs/*/**"] [tool.poetry.dependencies] python = "^3.7" @@ -25,6 +29,12 @@ dynaconf = "^3.0.0" sphinx = "^3.0" sphinx-autodoc-typehints = "^1.7" +[tool.poetry.scripts] +rwa-support-sessionservice = 'rwa.support.sessionservice.service:main' + +[tool.black] +line-length = 100 + [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" diff --git a/rwa/support/sessionservice/config.py b/rwa/support/sessionservice/config.py new file mode 100644 index 0000000..f0f8369 --- /dev/null +++ b/rwa/support/sessionservice/config.py @@ -0,0 +1,30 @@ +# This file is part of Remote Support Desktop +# https://gitlab.das-netzwerkteam.de/RemoteWebApp/rwa.support.sessionservice +# Copyright 2020, 2021 Jonathan Weth +# Copyright 2020 Daniel Teichmann +# Copyright 2020 Mike Gabriel +# SPDX-License-Identifier: GPL-2.0-or-later +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the +# Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from dynaconf import Dynaconf + +settings = Dynaconf( + envvar_prefix="RWA", settings_files=["/etc/rwa/support/sessionservice/settings.toml"] +) diff --git a/rwa/support/sessionservice/lock.py b/rwa/support/sessionservice/lock.py new file mode 100644 index 0000000..b4e384c --- /dev/null +++ b/rwa/support/sessionservice/lock.py @@ -0,0 +1,50 @@ +# This file is part of Remote Support Desktop +# https://gitlab.das-netzwerkteam.de/RemoteWebApp/rwa.support.sessionservice +# Copyright 2020, 2021 Jonathan Weth +# Copyright 2020 Daniel Teichmann +# Copyright 2020 Mike Gabriel +# SPDX-License-Identifier: GPL-2.0-or-later +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the +# Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import tempfile + +lock_dir_path = os.path.join(tempfile.gettempdir(), "rwa.support.sessionservice") +lock_file_path = os.path.join(lock_dir_path, "rwa.support.sessionservice.lock") + + +def lock(): + """Create lock file.""" + os.makedirs(lock_dir_path, exist_ok=True) + with open(lock_file_path, "w") as f: + f.write("lock") + + +def is_locked(): + """Check if the lock file exists.""" + return os.path.exists(lock_file_path) + + +def unlock(): + """Remove the lock file.""" + try: + os.remove(lock_file_path) + except FileNotFoundError: + pass diff --git a/rwa/support/sessionservice/log.py b/rwa/support/sessionservice/log.py new file mode 100644 index 0000000..ad8bf88 --- /dev/null +++ b/rwa/support/sessionservice/log.py @@ -0,0 +1,28 @@ +# This file is part of Remote Support Desktop +# https://gitlab.das-netzwerkteam.de/RemoteWebApp/rwa.support.sessionservice +# Copyright 2020, 2021 Jonathan Weth +# Copyright 2020 Daniel Teichmann +# Copyright 2020 Mike Gabriel +# SPDX-License-Identifier: GPL-2.0-or-later +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the +# Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging + +logging.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", level=logging.INFO) diff --git a/rwa/support/sessionservice/service.py b/rwa/support/sessionservice/service.py new file mode 100755 index 0000000..ae634c8 --- /dev/null +++ b/rwa/support/sessionservice/service.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python3 + +# This file is part of Remote Support Desktop +# https://gitlab.das-netzwerkteam.de/RemoteWebApp/rwa.support.sessionservice +# Copyright 2020, 2021 Jonathan Weth +# Copyright 2020 Daniel Teichmann +# Copyright 2020 Mike Gabriel +# SPDX-License-Identifier: GPL-2.0-or-later +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the +# Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +import json +import logging +import os +import signal +import tempfile +import time +from threading import Thread +from typing import Union, Any + +import dbus +import dbus.mainloop.glib +import dbus.service +from gi.repository import GLib + +from .lock import is_locked, lock, unlock +from .session import Session +from .trigger import TriggerServerThread + +ALLOW_ONLY_ONE_SESSION = True + + +class RWASupportSessionService(dbus.service.Object): + """D-Bus Session Service for RWA.Support. + + D-Bus namespace: ``org.ArcticaProject.RWASupportSessionService`` + + D-Bus object name: ``/RWASupportSessionService`` + + :param loop: GLib main loop running the service + :param mockup_mode: Starts the service in mock up mode + """ + + def __init__( + self, loop: GLib.MainLoop, mockup_mode: bool = False, one_time: bool = False + ): + self.loop = loop + self.mockup_mode = mockup_mode + self.one_time = one_time + + self.bus = dbus.SessionBus() + name = dbus.service.BusName("org.ArcticaProject.RWASupportSessionService", bus=self.bus) + + self.check_lock_thread = Thread(target=self._check_lock) + self.check_lock_thread.start() + + self.trigger_service = TriggerServerThread(self._trigger) + self.trigger_service.start() + + self.update_service_running = False + self.sessions = {} + super().__init__(name, "/RWASupportSessionService") + + logging.info("D-Bus service has been started.") + + @dbus.service.method("org.ArcticaProject.RWASupportSessionService", out_signature="s") + def start(self) -> str: + """Start a new remote session and register it in RWA.Support.WebApp. + + :return: Result as JSON (D-Bus string) + + **Structure of returned JSON (success):** + + :: + + {"status": "success", "id": , "url": "", "pin": } + + **Structure of returned JSON (error):** + + :: + + {"status": "error", "type": ""} + + **Possible choices for error types:** ``multiple``, ``connection`` + """ + if ALLOW_ONLY_ONE_SESSION and len(self.sessions.values()) > 0: + logging.warning( + "There is already one session running and the service is configured to allow only one " + "session, so this session won't be started." + ) + return json.dumps({"status": "error", "type": "multiple"}) + + # Start session + try: + session = Session(self.trigger_service.port, mockup_mode) + + # Add session to sessions list + self.sessions[session.pid] = session + + # Start session update service + self._ensure_update_service() + + return_json = session.client_meta + return_json["status"] = "success" + + logging.info( + f"New session #{session.pid} was started with meta {return_json}." + ) + + return json.dumps(return_json) + except ConnectionError: + pass + + return json.dumps({"status": "error", "type": "connection"}) + + @dbus.service.method("org.ArcticaProject.RWASupportSessionService", in_signature="i", out_signature="s") + def status(self, pid: int) -> str: + """Return the status of a session. + + .. note:: + + This uses the last status version got by the update service in the background. + + :param pid: (Process) ID of session (D-Bus integer) + :return: Session status as JSON (D-Bus string) + + **Structure of returned JSON:** + + :: + + {"id": , "status": } + + **Possible status options:** + + ============ ====================== + ``running`` The session is running and ready for connecting. + ``active`` The session is running and a the remote connected to the session. + ``stopped`` The session was stopped. + ``dead`` There was a problem, so that the session is dead. + ============ ====================== + """ + return self._get_status(pid) + + @dbus.service.method("org.ArcticaProject.RWASupportSessionService", in_signature="i", out_signature="s") + def refresh_status(self, pid: int) -> str: + """Same as :meth:`status`, but updates status from RWA.WebApp before returning it here. + """ + self._update_session(pid) + return self._get_status(pid) + + @dbus.service.method("org.ArcticaProject.RWASupportSessionService", in_signature="i", out_signature="s") + def stop(self, pid: int) -> str: + """Stop a remote session. + + :param pid: (Process) ID of session (D-Bus integer) + :return: Session status as JSON (D-Bus string) + + **Structure of returned JSON:** + + :: + + {"id": , "status": "stopped"} + """ + try: + session = self.sessions[pid] + except KeyError: + return json.dumps({"pid": pid, "status": "stopped"}, sort_keys=True) + session.stop() + return json.dumps({"id": pid, "status": "stopped"}, sort_keys=True) + + def _get_status(self, pid: int) -> str: + try: + session = self.sessions[pid] + except KeyError: + return json.dumps({"id": pid, "status": "dead"}, sort_keys=True) + return json.dumps(session.status) + + 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_session(self, pid: int): + """Update the status of a session.""" + + try: + session = self.sessions[pid] + except KeyError: + logging.info(f"Update status for session #{pid} …") + logging.warning(" Session is dead.") + return + + # Check if VNC process is still running + running = session.vnc_process_running + if running: + pass + elif session.status_text == "stopped" and session.pid in self.sessions: + logging.info(f"Update status for session #{pid} …") + logging.warning(" Session is dead.") + + del self.sessions[session.pid] + else: + logging.info(f"Update status for session #{pid} …") + logging.warning(" VNC was stopped, so session is dead.") + + session.stop() + del self.sessions[session.pid] + + def _update_sessions(self): + """Go through all running sessions and update their status using ``_update_session``.""" + logging.info("Started update service for sessions.") + while len(self.sessions.values()) > 0: + for session in list(self.sessions.values()): + self._update_session(session.pid) + + time.sleep(2) + + self.update_service_running = False + logging.info("Stopped update service for sessions.") + if self.one_time: + self._stop_all() + + def _trigger(self, session_id: int, data: dict, method: str = "trigger") -> Union[dict, bool]: + """Trigger a specific session via trigger token.""" + logging.info(f"Triggered with session ID {session_id} and {data}") + + for session in self.sessions.values(): + if session.session_id == session_id: + r = session.trigger(data, method) + logging.info(f"Session #{session.pid} matches the ID: {r}") + return r + + logging.warning(" No matching session found for this ID.") + return False + + def _stop_all(self): + """Stop all sessions and this daemon.""" + logging.info("Stop all sessions and exit service.") + for session in list(self.sessions.values()): + session.stop() + del self.sessions[session.pid] + self.trigger_service.shutdown() + self.loop.quit() + + def _check_lock(self): + """Check if lock file exists.""" + while True: + if not is_locked(): + logging.error("The lock file was removed, so stop this service.") + self._stop_all() + break + time.sleep(1) + + +def str2bool(v: Union[str, bool, int]) -> bool: + """Return true or false if the given string can be interpreted as a boolean otherwise raise an exception.""" + if isinstance(v, bool): + return v + if v.lower() in ("yes", "true", "t", "y", "1", 1): + return True + elif v.lower() in ("no", "false", "f", "n", "0", 0): + return False + else: + raise argparse.ArgumentTypeError("Boolean value expected.") + +def main(): + # Check for lock file + if is_locked(): + logging.error("The service is already running.") + exit(1) + + # Create lock file + lock() + + parser = argparse.ArgumentParser(description="D-Bus Session Service for RWA.Support") + parser.add_argument( + "-m", + "--mockup-mode", + type=str2bool, + nargs="?", + const=True, + default=False, + help="Activates mock up mode. Acts like the real session service but don't do changes or call RWA.", + ) + parser.add_argument( + "-o", + "--one-time", + type=str2bool, + nargs="?", + const=True, + default=False, + help="Runs as one-time-service. Stops after one session.", + ) + + args = parser.parse_args() + mockup_mode = args.mockup_mode + one_time = args.one_time + + if mockup_mode: + logging.warning( + "All API responses are faked and should NOT BE USED IN PRODUCTION!" + ) + + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + + loop = GLib.MainLoop() + object = RWASupportSessionService(loop, mockup_mode, one_time) + + def signal_handler(sig, frame): + logging.info("Service was terminated.") + object._stop_all() + + signal.signal(signal.SIGINT, signal_handler) + + loop.run() + + logging.info("Remove lock file ...") + unlock() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/rwa/support/sessionservice/session.py b/rwa/support/sessionservice/session.py new file mode 100644 index 0000000..1dfb6bb --- /dev/null +++ b/rwa/support/sessionservice/session.py @@ -0,0 +1,301 @@ +# This file is part of Remote Support Desktop +# https://gitlab.das-netzwerkteam.de/RemoteWebApp/rwa.support.sessionservice +# Copyright 2020, 2021 Jonathan Weth +# Copyright 2020 Daniel Teichmann +# Copyright 2020 Mike Gabriel +# SPDX-License-Identifier: GPL-2.0-or-later +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the +# Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import random +import secrets +import signal +import string +import subprocess +from typing import Dict, Union, Any + +import port_for +import psutil +import requests + +from .config import settings +from .log import logging +from .vnc import run_vnc, save_password + +API_SERVER = settings.get("api_url", "http://127.0.0.1") +BASE_URL = API_SERVER + "/app/rwasupport/api/" +REGISTER_URL = BASE_URL + "register/" +STOP_URL = BASE_URL + "stop/" +STATUS_URL = BASE_URL + "status/" +MARK_JOB_AS_DONE_URL = BASE_URL + "jobs/mark_as_done/" + +logging.info(f"Load API config: {API_SERVER}") + + +def random_digits(length: int): + return "".join(random.choice(string.digits) for _ in range(length)) + + +def get_desktop_dir(): + """Get desktop directory from xdg vars.""" + return ( + subprocess.check_output(["xdg-user-dir", "DESKTOP"]) + .decode() + .strip() + .replace("\n", "") + ) + + +class Session: + #: Session is running + STATUS_RUNNING = "running" + + #: Remote has joined the session + STATUS_JOINED = "active" + + def __init__(self, trigger_port: int, mockup_session: bool = False): + self.trigger_token = secrets.token_urlsafe(20) + self.trigger_port = trigger_port + self.done_jobs = [] + self.mockup_session = mockup_session + self.desktop_dir = get_desktop_dir() + self.desktop_dir = get_desktop_dir() + self._generate_password() + self._start_vnc() + self._register_session() + self.status_text = self.STATUS_RUNNING + + @property + def pid(self) -> int: + return self.vnc_pid + + @property + def port(self) -> int: + return self.ws_port + + @property + def _api_headers(self) -> Dict[str, str]: + return {"Authorization": f"Token {self.api_token}"} + + def _generate_password(self): + """Generate password for x11vnc and save it.""" + self.password = secrets.token_urlsafe(20) + + # Don't actually save a password if we just pretend to be a session. + if not self.mockup_session: + self.pw_filename = save_password(self.password) + + logging.info("The password for the session has been generated.") + + def _start_vnc(self): + """Start x11vnc server if not in mockup_session mode.""" + if not self.mockup_session: + process_info = run_vnc(self.pw_filename) + + logging.info("The VNC server has been started.") + + 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"] + else: + self.ws_port = port_for.select_random() + self.vnc_port = port_for.select_random() + + self.ws_pid = int(random_digits(5)) + self.vnc_pid = int(random_digits(5)) + + logging.info("The lock file for mocking a VNC server has been created.") + + # Create a temporary file to indicate that this process is still 'Running' + filename = f"/tmp/rwa/{str(self.ws_port) + str(self.vnc_port) + str(self.ws_pid) + str(self.vnc_pid)}.lock" + new_file = open(filename, "w") + new_file.write("this session is running") + + def _register_session(self): + """Register session in RWA.Support.WebApp if not in mockup_session mode.""" + if not self.mockup_session: + try: + r = requests.post( + REGISTER_URL, + json={ + "port": self.ws_port, + "pid": self.vnc_pid, + "trigger_port": self.trigger_port, + }, + ) + except requests.exceptions.ConnectionError: + raise ConnectionError() + + logging.info( + f"The session has been registered in RWA.Support.WebApp with status code {r.status_code} and response {r.content.decode()}." + ) + + if r.status_code != 200: + raise ConnectionError() + + self.meta = r.json() + self.session_id = self.meta["session_id"] + self.web_url = self.meta["url"] + self.api_token = self.meta["token"] + else: + logging.info(f"The session has pretended that he had created a session.") + self.meta = {} + self.session_id = int(random_digits(10)) + self.web_url = "http://example.com:" + random_digits(5) + "/app/rwasupport/test/" + self.api_token = secrets.token_urlsafe(10) + self.pin = int(random_digits(4)) + + def trigger(self, data: dict, method: str = "trigger") -> Union[dict, bool]: + """Event triggered by Django.""" + if method == "trigger" and data.get("token", "") == self.trigger_token: + self.pull() + return True + elif method == "authenticate" and data.get("pin", "") == self.pin: + return { + "password": self.password, + "trigger_token": self.trigger_token, + } + + + return False + def pull(self): + """Update status: Get status from Django.""" + if not self.mockup_session: + try: + r = requests.get( + STATUS_URL, params={"id": self.session_id}, headers=self._api_headers + ) + + logging.info( + f"The session has received its status from RWA.Support.WebApp with status code {r.status_code} and response {r.content.decode()}." + ) + except requests.ConnectionError: + pass + + if r.status_code in (401, 402, 403, 404, 405): + # Session doesn't exist anymore, so stop it local + self.stop(triggered=True) + else: + self.status_text = r.json()["status"] + self.jobs = r.json()["jobs"] + self._do_jobs() + + def _do_jobs(self): + """Go through all jobs and execute undone ones.""" + for job in self.jobs: + if not job["done"] or job["job_id"] in self.done_jobs: + job_type = job["job_type"] + if job_type == "file": + self._do_file_job(job) + + def _do_file_job(self, job): + """Download a file from server to the user's desktop.""" + logging.info( + f"The session has received a file job and is downloading it now ({job}):" + ) + subprocess.Popen(["wget", job["file"], "-P", self.desktop_dir]) + self._mark_job_as_done(job) + + def _mark_job_as_done(self, job): + """Mark a job as done (in this service and on the server).""" + self.done_jobs.append(job["job_id"]) + try: + r = requests.post( + MARK_JOB_AS_DONE_URL, + params={"id": job["job_id"]}, + headers=self._api_headers, + ) + logging.info( + f"The session has marked the job {job} as done in RWA.Support.WebApp with status code {r.status_code} and response {r.content.decode()}." + ) + except requests.ConnectionError: + pass + + def push(self): + """Update status: Push status to Django.""" + pass + + def stop(self, triggered: bool = False): + """Stop session and clean up.""" + if self.mockup_session: + logging.info("Mock session has been stopped by deleting its lock file.") + filename = f"/tmp/rwa/{str(self.ws_port) + str(self.vnc_port) + str(self.ws_pid) + str(self.vnc_pid)}.lock" + if os.path.isfile(filename): + os.remove(filename) + + # Delete self + del self + return + + # Kill websockify + if self.ws_pid in psutil.pids(): + os.kill(self.ws_pid, signal.SIGINT) + logging.info("The websockify server has been terminated.") + + # Kill VNC + if self.vnc_pid in psutil.pids(): + os.kill(self.vnc_pid, signal.SIGINT) + logging.info("The VNC server has been terminated.") + + # Delete PW file + if os.path.exists(self.pw_filename): + os.remove(self.pw_filename) + logging.info("The VNC server password file has been removed.") + + self.push() + + if not triggered: + try: + r = requests.post( + STOP_URL, params={"id": self.session_id}, headers=self._api_headers + ) + logging.info( + f"The stop action has been registered in RWA.Support.WebApp with status code {r.status_code} and response {r.content.decode()}." + ) + except requests.ConnectionError: + pass + + self.status_text = "stopped" + + # Delete self + del self + + @property + def vnc_process_running(self) -> bool: + """Check if the VNC process is still running.""" + if self.mockup_session: + filename = f"/tmp/rwa/{str(self.ws_port) + str(self.vnc_port) + str(self.ws_pid) + str(self.vnc_pid)}.lock" + return os.path.isfile(filename) + + 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) -> Dict[str, Union[str, int]]: + return {"id": self.pid, "session_id": self.session_id, "url": self.web_url, "pin": self.pin} + + @property + def status(self) -> Dict[str, Union[str, int]]: + return {"id": self.pid, "status": self.status_text} diff --git a/rwa/support/sessionservice/trigger.py b/rwa/support/sessionservice/trigger.py new file mode 100644 index 0000000..fa9734a --- /dev/null +++ b/rwa/support/sessionservice/trigger.py @@ -0,0 +1,80 @@ +# This file is part of Remote Support Desktop +# https://gitlab.das-netzwerkteam.de/RemoteWebApp/rwa.support.sessionservice +# Copyright 2020, 2021 Jonathan Weth +# Copyright 2020 Daniel Teichmann +# Copyright 2020 Mike Gabriel +# SPDX-License-Identifier: GPL-2.0-or-later +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the +# Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import threading +from typing import Any, Callable, Optional, Union +from wsgiref.simple_server import make_server + +import port_for +from flask import Flask, abort, request, jsonify + + +class TriggerServerThread(threading.Thread): + """Simple Flask server (wrapped as thread) for triggering actions on sessions.""" + + def __init__(self, trigger_method: Callable[[int, dict, Optional[str]], Union[dict, bool]]): + super().__init__() + self.port = port_for.select_random() + + app = Flask(__name__) + + @app.route("/", methods=["POST"]) + def trigger(): + json = request.json + token = json.get("token", "") + try: + session_id = int(json.get("session_id")) + r = trigger_method(session_id, {"token": token}, "trigger") + if r: + return "Successful triggered" + else: + return abort(403) + except (ValueError, TypeError): + return abort(404) + + @app.route("/authenticate/", methods=["POST"]) + def authenticate(): + json = request.json + try: + session_id = int(json.get("session_id")) + pin = int(json.get("pin", "")) + r = trigger_method(session_id, {"pin": pin}, "authenticate") + if r: + return jsonify(r) + else: + return abort(403) + except (ValueError, TypeError): + return abort(404) + + + self.srv = make_server("0.0.0.0", self.port, app) + self.ctx = app.app_context() + self.ctx.push() + + def run(self): + self.srv.serve_forever() + + def shutdown(self): + self.srv.shutdown() diff --git a/rwa/support/sessionservice/vnc.py b/rwa/support/sessionservice/vnc.py new file mode 100644 index 0000000..1725532 --- /dev/null +++ b/rwa/support/sessionservice/vnc.py @@ -0,0 +1,57 @@ +# This file is part of Remote Support Desktop +# https://gitlab.das-netzwerkteam.de/RemoteWebApp/rwa.support.sessionservice +# Copyright 2020, 2021 Jonathan Weth +# Copyright 2020 Daniel Teichmann +# Copyright 2020 Mike Gabriel +# SPDX-License-Identifier: GPL-2.0-or-later +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the +# Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +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}, + } diff --git a/service.py b/service.py deleted file mode 100755 index ada9b15..0000000 --- a/service.py +++ /dev/null @@ -1,337 +0,0 @@ -#!/usr/bin/env python3 - -# This file is part of Remote Support Desktop -# https://gitlab.das-netzwerkteam.de/RemoteWebApp/rwa.support.sessionservice -# Copyright 2020, 2021 Jonathan Weth -# Copyright 2020 Daniel Teichmann -# Copyright 2020 Mike Gabriel -# SPDX-License-Identifier: GPL-2.0-or-later -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the -# Free Software Foundation, Inc., -# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import argparse -import json -import logging -import os -import signal -import tempfile -import time -from threading import Thread -from typing import Union, Any - -import dbus -import dbus.mainloop.glib -import dbus.service -from gi.repository import GLib - -from lock import is_locked, lock, unlock -from session import Session -from trigger import TriggerServerThread - -ALLOW_ONLY_ONE_SESSION = True - - -class RWASupportSessionService(dbus.service.Object): - """D-Bus Session Service for RWA.Support. - - D-Bus namespace: ``org.ArcticaProject.RWASupportSessionService`` - - D-Bus object name: ``/RWASupportSessionService`` - - :param loop: GLib main loop running the service - :param mockup_mode: Starts the service in mock up mode - """ - - def __init__( - self, loop: GLib.MainLoop, mockup_mode: bool = False, one_time: bool = False - ): - self.loop = loop - self.mockup_mode = mockup_mode - self.one_time = one_time - - self.bus = dbus.SessionBus() - name = dbus.service.BusName("org.ArcticaProject.RWASupportSessionService", bus=self.bus) - - self.check_lock_thread = Thread(target=self._check_lock) - self.check_lock_thread.start() - - self.trigger_service = TriggerServerThread(self._trigger) - self.trigger_service.start() - - self.update_service_running = False - self.sessions = {} - super().__init__(name, "/RWASupportSessionService") - - logging.info("D-Bus service has been started.") - - @dbus.service.method("org.ArcticaProject.RWASupportSessionService", out_signature="s") - def start(self) -> str: - """Start a new remote session and register it in RWA.Support.WebApp. - - :return: Result as JSON (D-Bus string) - - **Structure of returned JSON (success):** - - :: - - {"status": "success", "id": , "url": "", "pin": } - - **Structure of returned JSON (error):** - - :: - - {"status": "error", "type": ""} - - **Possible choices for error types:** ``multiple``, ``connection`` - """ - if ALLOW_ONLY_ONE_SESSION and len(self.sessions.values()) > 0: - logging.warning( - "There is already one session running and the service is configured to allow only one " - "session, so this session won't be started." - ) - return json.dumps({"status": "error", "type": "multiple"}) - - # Start session - try: - session = Session(self.trigger_service.port, mockup_mode) - - # Add session to sessions list - self.sessions[session.pid] = session - - # Start session update service - self._ensure_update_service() - - return_json = session.client_meta - return_json["status"] = "success" - - logging.info( - f"New session #{session.pid} was started with meta {return_json}." - ) - - return json.dumps(return_json) - except ConnectionError: - pass - - return json.dumps({"status": "error", "type": "connection"}) - - @dbus.service.method("org.ArcticaProject.RWASupportSessionService", in_signature="i", out_signature="s") - def status(self, pid: int) -> str: - """Return the status of a session. - - .. note:: - - This uses the last status version got by the update service in the background. - - :param pid: (Process) ID of session (D-Bus integer) - :return: Session status as JSON (D-Bus string) - - **Structure of returned JSON:** - - :: - - {"id": , "status": } - - **Possible status options:** - - ============ ====================== - ``running`` The session is running and ready for connecting. - ``active`` The session is running and a the remote connected to the session. - ``stopped`` The session was stopped. - ``dead`` There was a problem, so that the session is dead. - ============ ====================== - """ - return self._get_status(pid) - - @dbus.service.method("org.ArcticaProject.RWASupportSessionService", in_signature="i", out_signature="s") - def refresh_status(self, pid: int) -> str: - """Same as :meth:`status`, but updates status from RWA.WebApp before returning it here. - """ - self._update_session(pid) - return self._get_status(pid) - - @dbus.service.method("org.ArcticaProject.RWASupportSessionService", in_signature="i", out_signature="s") - def stop(self, pid: int) -> str: - """Stop a remote session. - - :param pid: (Process) ID of session (D-Bus integer) - :return: Session status as JSON (D-Bus string) - - **Structure of returned JSON:** - - :: - - {"id": , "status": "stopped"} - """ - try: - session = self.sessions[pid] - except KeyError: - return json.dumps({"pid": pid, "status": "stopped"}, sort_keys=True) - session.stop() - return json.dumps({"id": pid, "status": "stopped"}, sort_keys=True) - - def _get_status(self, pid: int) -> str: - try: - session = self.sessions[pid] - except KeyError: - return json.dumps({"id": pid, "status": "dead"}, sort_keys=True) - return json.dumps(session.status) - - 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_session(self, pid: int): - """Update the status of a session.""" - - try: - session = self.sessions[pid] - except KeyError: - logging.info(f"Update status for session #{pid} …") - logging.warning(" Session is dead.") - return - - # Check if VNC process is still running - running = session.vnc_process_running - if running: - pass - elif session.status_text == "stopped" and session.pid in self.sessions: - logging.info(f"Update status for session #{pid} …") - logging.warning(" Session is dead.") - - del self.sessions[session.pid] - else: - logging.info(f"Update status for session #{pid} …") - logging.warning(" VNC was stopped, so session is dead.") - - session.stop() - del self.sessions[session.pid] - - def _update_sessions(self): - """Go through all running sessions and update their status using ``_update_session``.""" - logging.info("Started update service for sessions.") - while len(self.sessions.values()) > 0: - for session in list(self.sessions.values()): - self._update_session(session.pid) - - time.sleep(2) - - self.update_service_running = False - logging.info("Stopped update service for sessions.") - if self.one_time: - self._stop_all() - - def _trigger(self, session_id: int, data: dict, method: str = "trigger") -> Union[dict, bool]: - """Trigger a specific session via trigger token.""" - logging.info(f"Triggered with session ID {session_id} and {data}") - - for session in self.sessions.values(): - if session.session_id == session_id: - r = session.trigger(data, method) - logging.info(f"Session #{session.pid} matches the ID: {r}") - return r - - logging.warning(" No matching session found for this ID.") - return False - - def _stop_all(self): - """Stop all sessions and this daemon.""" - logging.info("Stop all sessions and exit service.") - for session in list(self.sessions.values()): - session.stop() - del self.sessions[session.pid] - self.trigger_service.shutdown() - self.loop.quit() - - def _check_lock(self): - """Check if lock file exists.""" - while True: - if not is_locked(): - logging.error("The lock file was removed, so stop this service.") - self._stop_all() - break - time.sleep(1) - - -def str2bool(v: Union[str, bool, int]) -> bool: - """Return true or false if the given string can be interpreted as a boolean otherwise raise an exception.""" - if isinstance(v, bool): - return v - if v.lower() in ("yes", "true", "t", "y", "1", 1): - return True - elif v.lower() in ("no", "false", "f", "n", "0", 0): - return False - else: - raise argparse.ArgumentTypeError("Boolean value expected.") - - -if __name__ == "__main__": - # Check for lock file - if is_locked(): - logging.error("The service is already running.") - exit(1) - - # Create lock file - lock() - - parser = argparse.ArgumentParser(description="D-Bus Session Service for RWA.Support") - parser.add_argument( - "-m", - "--mockup-mode", - type=str2bool, - nargs="?", - const=True, - default=False, - help="Activates mock up mode. Acts like the real session service but don't do changes or call RWA.", - ) - parser.add_argument( - "-o", - "--one-time", - type=str2bool, - nargs="?", - const=True, - default=False, - help="Runs as one-time-service. Stops after one session.", - ) - - args = parser.parse_args() - mockup_mode = args.mockup_mode - one_time = args.one_time - - if mockup_mode: - logging.warning( - "All API responses are faked and should NOT BE USED IN PRODUCTION!" - ) - - dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) - - loop = GLib.MainLoop() - object = RWASupportSessionService(loop, mockup_mode, one_time) - - def signal_handler(sig, frame): - logging.info("Service was terminated.") - object._stop_all() - - signal.signal(signal.SIGINT, signal_handler) - - loop.run() - - logging.info("Remove lock file ...") - unlock() diff --git a/session.py b/session.py deleted file mode 100644 index f977669..0000000 --- a/session.py +++ /dev/null @@ -1,301 +0,0 @@ -# This file is part of Remote Support Desktop -# https://gitlab.das-netzwerkteam.de/RemoteWebApp/rwa.support.sessionservice -# Copyright 2020, 2021 Jonathan Weth -# Copyright 2020 Daniel Teichmann -# Copyright 2020 Mike Gabriel -# SPDX-License-Identifier: GPL-2.0-or-later -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the -# Free Software Foundation, Inc., -# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os -import random -import secrets -import signal -import string -import subprocess -from typing import Dict, Union, Any - -import port_for -import psutil -import requests - -from config import settings -from log import logging -from vnc import run_vnc, save_password - -API_SERVER = settings.get("api_url", "http://127.0.0.1") -BASE_URL = API_SERVER + "/app/rwasupport/api/" -REGISTER_URL = BASE_URL + "register/" -STOP_URL = BASE_URL + "stop/" -STATUS_URL = BASE_URL + "status/" -MARK_JOB_AS_DONE_URL = BASE_URL + "jobs/mark_as_done/" - -logging.info(f"Load API config: {API_SERVER}") - - -def random_digits(length: int): - return "".join(random.choice(string.digits) for _ in range(length)) - - -def get_desktop_dir(): - """Get desktop directory from xdg vars.""" - return ( - subprocess.check_output(["xdg-user-dir", "DESKTOP"]) - .decode() - .strip() - .replace("\n", "") - ) - - -class Session: - #: Session is running - STATUS_RUNNING = "running" - - #: Remote has joined the session - STATUS_JOINED = "active" - - def __init__(self, trigger_port: int, mockup_session: bool = False): - self.trigger_token = secrets.token_urlsafe(20) - self.trigger_port = trigger_port - self.done_jobs = [] - self.mockup_session = mockup_session - self.desktop_dir = get_desktop_dir() - self.desktop_dir = get_desktop_dir() - self._generate_password() - self._start_vnc() - self._register_session() - self.status_text = self.STATUS_RUNNING - - @property - def pid(self) -> int: - return self.vnc_pid - - @property - def port(self) -> int: - return self.ws_port - - @property - def _api_headers(self) -> Dict[str, str]: - return {"Authorization": f"Token {self.api_token}"} - - def _generate_password(self): - """Generate password for x11vnc and save it.""" - self.password = secrets.token_urlsafe(20) - - # Don't actually save a password if we just pretend to be a session. - if not self.mockup_session: - self.pw_filename = save_password(self.password) - - logging.info("The password for the session has been generated.") - - def _start_vnc(self): - """Start x11vnc server if not in mockup_session mode.""" - if not self.mockup_session: - process_info = run_vnc(self.pw_filename) - - logging.info("The VNC server has been started.") - - 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"] - else: - self.ws_port = port_for.select_random() - self.vnc_port = port_for.select_random() - - self.ws_pid = int(random_digits(5)) - self.vnc_pid = int(random_digits(5)) - - logging.info("The lock file for mocking a VNC server has been created.") - - # Create a temporary file to indicate that this process is still 'Running' - filename = f"/tmp/rwa/{str(self.ws_port) + str(self.vnc_port) + str(self.ws_pid) + str(self.vnc_pid)}.lock" - new_file = open(filename, "w") - new_file.write("this session is running") - - def _register_session(self): - """Register session in RWA.Support.WebApp if not in mockup_session mode.""" - if not self.mockup_session: - try: - r = requests.post( - REGISTER_URL, - json={ - "port": self.ws_port, - "pid": self.vnc_pid, - "trigger_port": self.trigger_port, - }, - ) - except requests.exceptions.ConnectionError: - raise ConnectionError() - - logging.info( - f"The session has been registered in RWA.Support.WebApp with status code {r.status_code} and response {r.content.decode()}." - ) - - if r.status_code != 200: - raise ConnectionError() - - self.meta = r.json() - self.session_id = self.meta["session_id"] - self.web_url = self.meta["url"] - self.api_token = self.meta["token"] - else: - logging.info(f"The session has pretended that he had created a session.") - self.meta = {} - self.session_id = int(random_digits(10)) - self.web_url = "http://example.com:" + random_digits(5) + "/app/rwasupport/test/" - self.api_token = secrets.token_urlsafe(10) - self.pin = int(random_digits(4)) - - def trigger(self, data: dict, method: str = "trigger") -> Union[dict, bool]: - """Event triggered by Django.""" - if method == "trigger" and data.get("token", "") == self.trigger_token: - self.pull() - return True - elif method == "authenticate" and data.get("pin", "") == self.pin: - return { - "password": self.password, - "trigger_token": self.trigger_token, - } - - - return False - def pull(self): - """Update status: Get status from Django.""" - if not self.mockup_session: - try: - r = requests.get( - STATUS_URL, params={"id": self.session_id}, headers=self._api_headers - ) - - logging.info( - f"The session has received its status from RWA.Support.WebApp with status code {r.status_code} and response {r.content.decode()}." - ) - except requests.ConnectionError: - pass - - if r.status_code in (401, 402, 403, 404, 405): - # Session doesn't exist anymore, so stop it local - self.stop(triggered=True) - else: - self.status_text = r.json()["status"] - self.jobs = r.json()["jobs"] - self._do_jobs() - - def _do_jobs(self): - """Go through all jobs and execute undone ones.""" - for job in self.jobs: - if not job["done"] or job["job_id"] in self.done_jobs: - job_type = job["job_type"] - if job_type == "file": - self._do_file_job(job) - - def _do_file_job(self, job): - """Download a file from server to the user's desktop.""" - logging.info( - f"The session has received a file job and is downloading it now ({job}):" - ) - subprocess.Popen(["wget", job["file"], "-P", self.desktop_dir]) - self._mark_job_as_done(job) - - def _mark_job_as_done(self, job): - """Mark a job as done (in this service and on the server).""" - self.done_jobs.append(job["job_id"]) - try: - r = requests.post( - MARK_JOB_AS_DONE_URL, - params={"id": job["job_id"]}, - headers=self._api_headers, - ) - logging.info( - f"The session has marked the job {job} as done in RWA.Support.WebApp with status code {r.status_code} and response {r.content.decode()}." - ) - except requests.ConnectionError: - pass - - def push(self): - """Update status: Push status to Django.""" - pass - - def stop(self, triggered: bool = False): - """Stop session and clean up.""" - if self.mockup_session: - logging.info("Mock session has been stopped by deleting its lock file.") - filename = f"/tmp/rwa/{str(self.ws_port) + str(self.vnc_port) + str(self.ws_pid) + str(self.vnc_pid)}.lock" - if os.path.isfile(filename): - os.remove(filename) - - # Delete self - del self - return - - # Kill websockify - if self.ws_pid in psutil.pids(): - os.kill(self.ws_pid, signal.SIGINT) - logging.info("The websockify server has been terminated.") - - # Kill VNC - if self.vnc_pid in psutil.pids(): - os.kill(self.vnc_pid, signal.SIGINT) - logging.info("The VNC server has been terminated.") - - # Delete PW file - if os.path.exists(self.pw_filename): - os.remove(self.pw_filename) - logging.info("The VNC server password file has been removed.") - - self.push() - - if not triggered: - try: - r = requests.post( - STOP_URL, params={"id": self.session_id}, headers=self._api_headers - ) - logging.info( - f"The stop action has been registered in RWA.Support.WebApp with status code {r.status_code} and response {r.content.decode()}." - ) - except requests.ConnectionError: - pass - - self.status_text = "stopped" - - # Delete self - del self - - @property - def vnc_process_running(self) -> bool: - """Check if the VNC process is still running.""" - if self.mockup_session: - filename = f"/tmp/rwa/{str(self.ws_port) + str(self.vnc_port) + str(self.ws_pid) + str(self.vnc_pid)}.lock" - return os.path.isfile(filename) - - 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) -> Dict[str, Union[str, int]]: - return {"id": self.pid, "session_id": self.session_id, "url": self.web_url, "pin": self.pin} - - @property - def status(self) -> Dict[str, Union[str, int]]: - return {"id": self.pid, "status": self.status_text} diff --git a/trigger.py b/trigger.py deleted file mode 100644 index fa9734a..0000000 --- a/trigger.py +++ /dev/null @@ -1,80 +0,0 @@ -# This file is part of Remote Support Desktop -# https://gitlab.das-netzwerkteam.de/RemoteWebApp/rwa.support.sessionservice -# Copyright 2020, 2021 Jonathan Weth -# Copyright 2020 Daniel Teichmann -# Copyright 2020 Mike Gabriel -# SPDX-License-Identifier: GPL-2.0-or-later -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the -# Free Software Foundation, Inc., -# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import threading -from typing import Any, Callable, Optional, Union -from wsgiref.simple_server import make_server - -import port_for -from flask import Flask, abort, request, jsonify - - -class TriggerServerThread(threading.Thread): - """Simple Flask server (wrapped as thread) for triggering actions on sessions.""" - - def __init__(self, trigger_method: Callable[[int, dict, Optional[str]], Union[dict, bool]]): - super().__init__() - self.port = port_for.select_random() - - app = Flask(__name__) - - @app.route("/", methods=["POST"]) - def trigger(): - json = request.json - token = json.get("token", "") - try: - session_id = int(json.get("session_id")) - r = trigger_method(session_id, {"token": token}, "trigger") - if r: - return "Successful triggered" - else: - return abort(403) - except (ValueError, TypeError): - return abort(404) - - @app.route("/authenticate/", methods=["POST"]) - def authenticate(): - json = request.json - try: - session_id = int(json.get("session_id")) - pin = int(json.get("pin", "")) - r = trigger_method(session_id, {"pin": pin}, "authenticate") - if r: - return jsonify(r) - else: - return abort(403) - except (ValueError, TypeError): - return abort(404) - - - self.srv = make_server("0.0.0.0", self.port, app) - self.ctx = app.app_context() - self.ctx.push() - - def run(self): - self.srv.serve_forever() - - def shutdown(self): - self.srv.shutdown() diff --git a/vnc.py b/vnc.py deleted file mode 100644 index 1725532..0000000 --- a/vnc.py +++ /dev/null @@ -1,57 +0,0 @@ -# This file is part of Remote Support Desktop -# https://gitlab.das-netzwerkteam.de/RemoteWebApp/rwa.support.sessionservice -# Copyright 2020, 2021 Jonathan Weth -# Copyright 2020 Daniel Teichmann -# Copyright 2020 Mike Gabriel -# SPDX-License-Identifier: GPL-2.0-or-later -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the -# Free Software Foundation, Inc., -# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -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}, - } -- cgit v1.2.3