aboutsummaryrefslogtreecommitdiff
path: root/rwa/support/sessionservice/session.py
diff options
context:
space:
mode:
Diffstat (limited to 'rwa/support/sessionservice/session.py')
-rw-r--r--rwa/support/sessionservice/session.py301
1 files changed, 301 insertions, 0 deletions
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}