diff options
author | Jonathan Weth <git@jonathanweth.de> | 2021-06-23 12:33:28 +0200 |
---|---|---|
committer | Jonathan Weth <git@jonathanweth.de> | 2021-06-23 12:33:28 +0200 |
commit | ed1d15aa02b3c7c1350a5204861d1f8678550fbb (patch) | |
tree | 51d4aa7cdee4f90b5f2c3a4188cb80519dd2342d /rwa/support | |
parent | 74e02d1953c1ee03a4e7dfc73e80318a24ba56a7 (diff) | |
download | RWA.Support.SessionService-ed1d15aa02b3c7c1350a5204861d1f8678550fbb.tar.gz RWA.Support.SessionService-ed1d15aa02b3c7c1350a5204861d1f8678550fbb.tar.bz2 RWA.Support.SessionService-ed1d15aa02b3c7c1350a5204861d1f8678550fbb.zip |
Restructure project directory and provide script for session service
Diffstat (limited to 'rwa/support')
-rw-r--r-- | rwa/support/sessionservice/config.py | 30 | ||||
-rw-r--r-- | rwa/support/sessionservice/lock.py | 50 | ||||
-rw-r--r-- | rwa/support/sessionservice/log.py | 28 | ||||
-rwxr-xr-x | rwa/support/sessionservice/service.py | 340 | ||||
-rw-r--r-- | rwa/support/sessionservice/session.py | 301 | ||||
-rw-r--r-- | rwa/support/sessionservice/trigger.py | 80 | ||||
-rw-r--r-- | rwa/support/sessionservice/vnc.py | 57 |
7 files changed, 886 insertions, 0 deletions
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 <dev@jonathanweth.de> +# Copyright 2020 Daniel Teichmann <daniel.teichmann@das-netzwerkteam.de> +# Copyright 2020 Mike Gabriel <mike.gabriel@das-netzwerkteam.de> +# 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 <https://www.gnu.org/licenses/>. + +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 <dev@jonathanweth.de> +# Copyright 2020 Daniel Teichmann <daniel.teichmann@das-netzwerkteam.de> +# Copyright 2020 Mike Gabriel <mike.gabriel@das-netzwerkteam.de> +# 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 <https://www.gnu.org/licenses/>. + +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 <dev@jonathanweth.de> +# Copyright 2020 Daniel Teichmann <daniel.teichmann@das-netzwerkteam.de> +# Copyright 2020 Mike Gabriel <mike.gabriel@das-netzwerkteam.de> +# 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 <https://www.gnu.org/licenses/>. + +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 <dev@jonathanweth.de> +# Copyright 2020 Daniel Teichmann <daniel.teichmann@das-netzwerkteam.de> +# Copyright 2020 Mike Gabriel <mike.gabriel@das-netzwerkteam.de> +# 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 <https://www.gnu.org/licenses/>. + +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": <pid>, "url": "<url>", "pin": <pin>} + + **Structure of returned JSON (error):** + + :: + + {"status": "error", "type": "<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": <pid>, "status": <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": <pid>, "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 <dev@jonathanweth.de> +# Copyright 2020 Daniel Teichmann <daniel.teichmann@das-netzwerkteam.de> +# Copyright 2020 Mike Gabriel <mike.gabriel@das-netzwerkteam.de> +# 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 <https://www.gnu.org/licenses/>. + +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 <dev@jonathanweth.de> +# Copyright 2020 Daniel Teichmann <daniel.teichmann@das-netzwerkteam.de> +# Copyright 2020 Mike Gabriel <mike.gabriel@das-netzwerkteam.de> +# 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 <https://www.gnu.org/licenses/>. + +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 <dev@jonathanweth.de> +# Copyright 2020 Daniel Teichmann <daniel.teichmann@das-netzwerkteam.de> +# Copyright 2020 Mike Gabriel <mike.gabriel@das-netzwerkteam.de> +# 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 <https://www.gnu.org/licenses/>. + +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}, + } |