From 6c5bd3fe5dd5dc6eb9ad8d44c588e3cd8b01fdcd Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Tue, 29 Jun 2021 16:35:33 +0200
Subject: [PATCH] Implement API handshake

---
 rwa/support/sessionservice/config.py  |  4 ++
 rwa/support/sessionservice/service.py | 88 +++++++++++++++++++++++++--
 rwa/support/sessionservice/session.py |  3 +-
 3 files changed, 88 insertions(+), 7 deletions(-)

diff --git a/rwa/support/sessionservice/config.py b/rwa/support/sessionservice/config.py
index de5518d..7863751 100644
--- a/rwa/support/sessionservice/config.py
+++ b/rwa/support/sessionservice/config.py
@@ -28,3 +28,7 @@ import usersettings
 user_settings = usersettings.Settings("org.ArcticaProject.RWASupportSessionService")
 user_settings.add_setting("web_app_hosts", list, ["http://127.0.0.1:8000"])
 user_settings.load_settings()
+
+SUPPORTED_API_VERSIONS = [1]
+API_PATH = "/app/rwasupport/api/"
+ALLOW_ONLY_ONE_SESSION = True
diff --git a/rwa/support/sessionservice/service.py b/rwa/support/sessionservice/service.py
index 52bf627..3305223 100755
--- a/rwa/support/sessionservice/service.py
+++ b/rwa/support/sessionservice/service.py
@@ -30,21 +30,20 @@ import logging
 import signal
 import time
 from threading import Thread
-from typing import Union
+from typing import Dict, Union
 
 import click
 import dbus
 import dbus.mainloop.glib
 import dbus.service
+import requests
 from gi.repository import GLib
 
-from .config import user_settings
+from .config import ALLOW_ONLY_ONE_SESSION, API_PATH, SUPPORTED_API_VERSIONS, user_settings
 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.
@@ -99,6 +98,55 @@ class RWASupportSessionService(dbus.service.Object):
         """
         return self._get_web_app_hosts()
 
+    def _do_api_handshake(self, host: str) -> Dict[str, str]:
+        """Contact a RWA.Support.WebApp host and find out API version.
+
+        :param host: The full hostname.
+        :return: Status information as dictionary.
+
+        **Structure of returned JSON (success):**
+
+        ::
+
+            {"status": "success", "type": "valid_host"}
+
+         **Structure of returned JSON (error):**
+
+        ::
+
+            {"status": "error", "type": "<type>"}
+
+        **Possible choices for error types:**
+
+            * ``connection``
+            * ``permission_denied``
+            * ``unsupported_server``
+        """
+        url = host + API_PATH + "handshake/"
+        logging.info(f"API handshake with {url} ...")
+        try:
+            r = requests.post(url)
+
+        except requests.exceptions.ConnectionError:
+            logging.warning("  resulted in a connection error.")
+            return {"status": "error", "type": "connection"}
+
+        if not r.ok:
+            logging.warning("  resulted in a connection error.")
+            return {"status": "error", "type": "connection"}
+
+        if not r.json()["allowed"]:
+            logging.warning("  was not permitted.")
+            return {"status": "error", "type": "permission_denied"}
+
+        if r.json().get("api_version") not in SUPPORTED_API_VERSIONS:
+            logging.warning("  resulted in a incompatible API version.")
+            return {"status": "error", "type": "unsupported_server"}
+
+        logging.info("  was successful.")
+
+        return {"status": "success", "type": "valid_host"}
+
     @dbus.service.method(
         "org.ArcticaProject.RWASupportSessionService", in_signature="s", out_signature="s"
     )
@@ -108,12 +156,29 @@ class RWASupportSessionService(dbus.service.Object):
         :param host: Exact hostname of the RWA.Support.WebApp host (D-Bus string)
         :return: All registered hosts as JSON array (D-Bus string)
 
-        **Structure of returned JSON:**
+        **Structure of returned JSON (success):**
 
         ::
 
             ["https://example.org", "http://127.0.0.1:8000"]
+
+        **Structure of returned JSON (error):**
+
+        ::
+
+            {"status": "error", "type": "<type>"}
+
+        **Possible choices for error types:**
+
+            * ``connection``
+            * ``permission_denied``
+            * ``unsupported_server``
         """
+        # Check host by doing a handshake
+        res = self._do_api_handshake(host)
+        if res["status"] == "error":
+            return json.dumps(res)
+
         user_settings.web_app_hosts.append(host)
         user_settings.save_settings()
         return self._get_web_app_hosts()
@@ -158,7 +223,13 @@ class RWASupportSessionService(dbus.service.Object):
 
             {"status": "error", "type": "<type>"}
 
-        **Possible choices for error types:** ``multiple``, ``connection``
+        **Possible choices for error types:**
+
+            * ``multiple``
+            * ``connection``
+            * ``host_not_found``
+            * ``permission_denied``
+            * ``unsupported_server``
         """
         if ALLOW_ONLY_ONE_SESSION and len(self.sessions.values()) > 0:
             logging.warning(
@@ -174,6 +245,11 @@ class RWASupportSessionService(dbus.service.Object):
         except IndexError:
             return json.dumps({"status": "error", "type": "host_not_found"})
 
+        # Check host by doing a handshake
+        res = self._do_api_handshake(host)
+        if res["status"] == "error":
+            return json.dumps(res)
+
         # Start session
         try:
             session = Session(host, self.trigger_service.port, self.mockup_mode)
diff --git a/rwa/support/sessionservice/session.py b/rwa/support/sessionservice/session.py
index 0126800..aa97086 100644
--- a/rwa/support/sessionservice/session.py
+++ b/rwa/support/sessionservice/session.py
@@ -35,6 +35,7 @@ import port_for
 import psutil
 import requests
 
+from .config import API_PATH
 from .lock import TEMP_DIR_PATH
 from .log import logging
 from .vnc import run_vnc, save_password
@@ -59,7 +60,7 @@ class Session:
 
     def __init__(self, host: str, trigger_port: int, mockup_session: bool = False):
         self.host = host
-        self.BASE_URL = self.host + "/app/rwasupport/api/"
+        self.BASE_URL = self.host + API_PATH
         self.REGISTER_URL = self.BASE_URL + "register/"
         self.STOP_URL = self.BASE_URL + "stop/"
         self.STATUS_URL = self.BASE_URL + "status/"
-- 
GitLab